#pragma once

#include "core/arena_allocator.h"
#include "core/defer.h"
#include "core/log.h"
#include "core/string.h"
#include "renderer/editor/world_viewer.h"
#include "renderer/particle_system.h"
#include "renderer/render_module.h"

// TODO bugs
// errors in imports in compileIR results in crash

// TODO
// * and, or, not
// * unary in IR

// TODO maybe / low prio
// * spline, mesh, terrain
// * saturate, floor, round
// * preview for world space moving ribbons
// * emit_ribbon()
// * kill in ribbons
// * multiply-add
// * while, for cycles
// * autocomplete
// * debugger
// * global update - runs once per frame on the whole emitter, can prepare some global data
// * create mesh from script?

namespace Lumix {

struct ParticleScriptToken {
	enum Type {
		EOF, ERROR, SEMICOLON, COMMA, COLON, DOT,
		LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE,
		STAR, SLASH, MINUS, PLUS, EQUAL, PERCENT, GT, LT,
		NUMBER, STRING, IDENTIFIER,

		// keywords
		CONST, GLOBAL, EMITTER, FN, VAR, OUT, IN, LET, RETURN, IMPORT, IF, ELSE
	};

	Type type;
	StringView value;
};

struct ParticleScriptTokenizer {
	using Token = ParticleScriptToken;
	
	enum class Operators : char {
		ADD = '+',
		SUB = '-',
		DIV = '/',
		MUL = '*',
		MOD = '%',
		LT = '<',
		GT = '>',
	};
	
	StringView m_document;
	const char* m_start_token;
	const char* m_current;
	Token m_current_token;

	void skipWhitespaces() {
		while (m_current != m_document.end && isWhitespace(*m_current)) ++m_current;
		if (m_current < m_document.end - 1
			&& m_current[0] == '/'
			&& m_current[1] == '/') 
		{
			m_current += 2;
			while (m_current != m_document.end && *m_current != '\n') {
				++m_current;
			}
			skipWhitespaces();
		}
	}

	Token makeToken(Token::Type type) {
		Token res;
		res.type = type;
		res.value.begin = m_start_token;
		res.value.end = m_current;
		if (type == Token::STRING) {
			++res.value.begin;
			--res.value.end;
		}
		return res;
	}

	char advance() {
		ASSERT(m_current < m_document.end);
		char c = m_current[0];
		++m_current;
		return c;
	}

	static bool isDigit(char c) { return c >= '0' && c <= '9'; }

	char peekChar() {
		if (m_current == m_document.end) return 0;
		return m_current[0];
	}

	char peekNextChar() {
		if (m_current + 1 >= m_document.end) return 0;
		return m_current[1];
	}

	Token numberToken() {
		while (isDigit(peekChar())) advance();
		if (peekChar() == '.') {
			advance();
			if (!isDigit(peekChar())) return makeToken(Token::ERROR);
			advance();
			while (isDigit(peekChar())) advance();
		}
		return makeToken(Token::NUMBER);
	}

	Token stringToken() {
		while (m_current != m_document.end && m_current[0] != '"') {
			++m_current;
		}
		if (m_current == m_document.end) return makeToken(Token::ERROR);
		advance();
		return makeToken(Token::STRING);
	}

	Token checkKeyword(const char* remaining, i32 start, i32 len, Token::Type type) {
		if (m_current - m_start_token != start + len) return makeToken(Token::IDENTIFIER);
		if (memcmp(m_start_token + start, remaining, len) != 0) return makeToken(Token::IDENTIFIER);
		return makeToken(type);
	}

	static bool isIdentifierChar(char c) {
		return isLetter(c) || isDigit(c) || c == '_';
	}

	Token identifierOrKeywordToken() {
		while (isIdentifierChar(peekChar())) advance();

		switch (m_start_token[0]) {
			case 'c': return checkKeyword("onst", 1, 4, Token::CONST);
			case 'e': {
				if (m_current - m_start_token < 2) return makeToken(Token::IDENTIFIER);
				switch (m_start_token[1]) {
					case 'm': return checkKeyword("itter", 2, 5, Token::EMITTER);
					case 'l': return checkKeyword("se", 2, 2, Token::ELSE);
				}
			}
			case 'f': return checkKeyword("n", 1, 1, Token::FN);
			case 'g': return checkKeyword("lobal", 1, 5, Token::GLOBAL);
			case 'i': {
				if (m_current - m_start_token < 2) return makeToken(Token::IDENTIFIER);
				switch (m_start_token[1]) {
					case 'f': return checkKeyword("", 2, 0, Token::IF);
					case 'n': return checkKeyword("", 2, 0, Token::IN);
					case 'm': return checkKeyword("port", 2, 4, Token::IMPORT);
				}
				return makeToken(Token::IDENTIFIER);
			}
			case 'l': return checkKeyword("et", 1, 2, Token::LET);
			case 'o': return checkKeyword("ut", 1, 2, Token::OUT);
			case 'r': return checkKeyword("eturn", 1, 5, Token::RETURN);
			case 'v': return checkKeyword("ar", 1, 2, Token::VAR);
		}
		return makeToken(Token::IDENTIFIER);
	}

	Token nextToken() {
		skipWhitespaces();
		Token res;
		m_start_token = m_current;
		if (m_current == m_document.end) return makeToken(Token::EOF);

		char c = advance();
		if (isDigit(c)) return numberToken();
		if (isLetter(c) || c == '_') return identifierOrKeywordToken();
		
		switch (c) {
			case '"': return stringToken();
			case '.': return makeToken(Token::DOT);
			case ';': return makeToken(Token::SEMICOLON);
			case ':': return makeToken(Token::COLON);
			case '{': return makeToken(Token::LEFT_BRACE);
			case '}': return makeToken(Token::RIGHT_BRACE);
			case '(': return makeToken(Token::LEFT_PAREN);
			case ')': return makeToken(Token::RIGHT_PAREN);
			case '-': return makeToken(Token::MINUS);
			case '+': return makeToken(Token::PLUS);
			case '*': return makeToken(Token::STAR);
			case '/': return makeToken(Token::SLASH);
			case '%': return makeToken(Token::PERCENT);
			case '=': return makeToken(Token::EQUAL);
			case ',': return makeToken(Token::COMMA);
			case '>': return makeToken(Token::GT);
			case '<': return makeToken(Token::LT);
		}
		
		return makeToken(Token::ERROR);
	}
};

struct ParticleScriptCompiler {
	using InstructionType = ParticleSystemResource::InstructionType;
	using DataStream = ParticleSystemResource::DataStream;
	using Token = ParticleScriptToken;
	using Operators = ParticleScriptTokenizer::Operators;

    enum class ValueType {
        FLOAT,
        FLOAT3,
        FLOAT4,

		VOID
    };

	enum class EntryPoint {
		EMIT,
		UPDATE,
		OUTPUT,
		GLOBAL
	};

	struct SysCall {
        InstructionType instruction;
        bool returns_value;
		u32 num_args;
		u32 valid_entry_points =
			  (1 << u32(EntryPoint::EMIT)) 
			| (1 << u32(EntryPoint::UPDATE))
			| (1 << u32(EntryPoint::OUTPUT))
			;
    };

    struct Constant {
        StringView name;
        ValueType type;
        float value[4];
    };

	enum class VariableFamily {
		OUTPUT,
		CHANNEL,
		INPUT,
		LOCAL,
		GLOBAL
	};

	struct Local {
		StringView name;
		ValueType type;
		i32 registers[4] = {-1, -1, -1, -1};
		bool is_const[4] = {true, true, true, true};
		float values[4] = {};
	};

	struct BlockNode;

	struct Function {
		Function(IAllocator& allocator) : args(allocator) {}
		StringView name;
		Array<StringView> args;
		BlockNode* block = nullptr;
	};

	struct Variable {
		StringView name;
		ValueType type;
		i32 offset = 0;

        i32 getOffsetSub(u32 sub) const {
            switch (type) {
                case ValueType::VOID: ASSERT(false); return offset;
                case ValueType::FLOAT: return offset;
                case ValueType::FLOAT3: return offset + minimum(sub, 2);
                case ValueType::FLOAT4: return offset + minimum(sub, 3);
            }
            ASSERT(false);
            return offset;
        }
	};

	struct Emitter {
		Emitter(IAllocator& allocator)
			: m_outputs(allocator)
			, m_update(allocator)
			, m_emit(allocator)
			, m_output(allocator)
			, m_inputs(allocator)
			, m_vars(allocator)
		{}

		StringView m_name;
		Path m_material;
		Path m_mesh;
		OutputMemoryStream m_update;
		OutputMemoryStream m_emit;
		OutputMemoryStream m_output;
		u32 m_num_update_registers = 0;
		u32 m_num_emit_registers = 0;
		u32 m_num_output_registers = 0;
		u32 m_num_update_instructions = 0;
		u32 m_num_emit_instructions = 0;
		u32 m_num_output_instructions = 0;
		Array<Variable> m_vars;
		Array<Variable> m_outputs;
		Array<Variable> m_inputs;
		u32 m_init_emit_count = 0;
		bool m_emit_on_move = false;
		float m_emit_per_second = 0;
		u32 m_max_ribbons = 0;
		u32 m_max_ribbon_length = 0;
		u32 m_init_ribbons_count = 0;
	};

	struct CompileContext {
		CompileContext(ParticleScriptCompiler& compiler) : compiler(compiler) {} 
		ParticleScriptCompiler& compiler; 
		const Function* function = nullptr;
		Emitter* emitter = nullptr;
		Emitter* emitted = nullptr;
		BlockNode* block = nullptr;
		EntryPoint entry_point;


		u32 value_counter = 0;
	};

	struct Node {
		enum Type {
			UNARY_OPERATOR,
			BINARY_OPERATOR,
			LITERAL,
			RETURN,
			FUNCTION_ARG,
			ASSIGN,
			VARIABLE,
			SWIZZLE,
			SYSCALL,
			SYSTEM_VALUE,
			COMPOUND,
			EMITTER_REF,
			BLOCK,
			FUNCTION_CALL,
			IF
		};

		Node(Type type, Token token) : type(type), token(token) {}
		virtual ~Node() {}
		
		Type type;
		Token token;
	};

	struct SystemValueNode : Node {
		SystemValueNode(Token token) : Node(Node::SYSTEM_VALUE, token) {}
		ParticleSystemValues value;
	};

	struct CompoundNode : Node {
		CompoundNode(Token token, IAllocator& allocator) : Node(Node::COMPOUND, token), elements(allocator) {}
		Array<Node*> elements;
	};

	struct BlockNode : Node {
		BlockNode(Token token, IAllocator& allocator) : Node(Node::BLOCK, token), statements(allocator), locals(allocator) {}
		Array<Node*> statements;
		Array<Local> locals;
		BlockNode* parent = nullptr;
	};

	struct IfNode : Node {
		IfNode(Token token) : Node(Node::IF, token) {}
		Node* condition = nullptr;
		BlockNode* true_block = nullptr;
		BlockNode* false_block = nullptr;
	};\

	struct SysCallNode : Node {
		SysCallNode(Token token, IAllocator& allocator) : Node(Node::SYSCALL, token), args(allocator) {}
		SysCall function;
		Array<Node*> args;
		Node* after_block = nullptr;
	};

	struct FunctionCallNode : Node {
		FunctionCallNode(Token token, IAllocator& allocator) : Node(Node::FUNCTION_CALL, token), args(allocator) {}
		i32 function_index;
		Array<Node*> args;
	};

	struct ReturnNode : Node {
		ReturnNode(Token token) : Node(Node::RETURN, token) {}
		Node* value;
	};

	struct EmitterRefNode : Node {
		EmitterRefNode(Token token) : Node(Node::EMITTER_REF, token) {}
		i32 index;
	};

	struct VariableNode : Node {
		VariableNode(Token token) : Node(Node::VARIABLE, token) {}
		i32 index;
		BlockNode* block; // for locals
		VariableFamily family;
	};

	struct SwizzleNode : Node {
		SwizzleNode(Token token) : Node(Node::SWIZZLE, token) {}
		Node* left;
	};

	struct FunctionArgNode : Node {
		FunctionArgNode(Token token) : Node(Node::FUNCTION_ARG, token) {}
		i32 index;
	};

	struct LiteralNode : Node {
		LiteralNode(Token token) : Node(Node::LITERAL, token) {}
		float value;
	};

	struct UnaryOperatorNode : Node {
		UnaryOperatorNode(Token token) : Node(Node::UNARY_OPERATOR, token) {}
		Node* right;
		Operators op;
	};

	struct AssignNode : Node {
		AssignNode(Token token) : Node(Node::ASSIGN, token) {}
		Node* left;
		Node* right;
	};

	struct BinaryOperatorNode : Node {
		BinaryOperatorNode(Token token) : Node(Node::BINARY_OPERATOR, token) {}
		Node* left;
		Node* right;
		Operators op;
	};
	
	ParticleScriptCompiler(StudioApp& app, IAllocator& allocator)
		: m_app(app)
		, m_allocator(allocator)
		, m_emitters(allocator)
		, m_constants(allocator)
		, m_globals(allocator)
		, m_functions(allocator)
		, m_arena_allocator(1024 * 1024 * 256, m_allocator, "particle_script_compiler")
		, m_imports(allocator)
	{
	}

	~ParticleScriptCompiler() {
		m_arena_allocator.reset();
	}

	ValueType parseType() {
		StringView type;
		if (!consume(Token::IDENTIFIER, type)) return ValueType::FLOAT;
		if (equalStrings(type, "float")) return ValueType::FLOAT;
		if (equalStrings(type, "float3")) return ValueType::FLOAT3;
		if (equalStrings(type, "float4")) return ValueType::FLOAT4;
		error(type, "Unknown type");
		return ValueType::FLOAT;
	}

	void variableDeclaration(Array<Variable>& vars) {
		u32 offset = 0;
		if (!vars.empty()) {
			const Variable& var = vars.last();
			offset = var.offset;
			switch (var.type) {
				case ValueType::VOID: ASSERT(false); break;
				case ValueType::FLOAT: ++offset; break;
				case ValueType::FLOAT3: offset += 3; break;
				case ValueType::FLOAT4: offset += 4; break;
			}
		}
		Variable& var = vars.emplace();
		if (!consume(Token::IDENTIFIER, var.name)) return;
		consume(Token::COLON);
		var.type = parseType();
		var.offset = offset;
	}

	i32 getFunctionIndex(StringView ident) const {
		for (Function& fn : m_functions) {
			if (equalStrings(fn.name, ident)) return i32(&fn - m_functions.begin());
		}
		return -1;
	}

	static i32 getArgumentIndex(const Function& fn, StringView ident) {
		for (i32 i = 0, c = fn.args.size(); i < c; ++i) {
			if (equalStrings(fn.args[i], ident)) return i;
		}
		return -1;
	}

	i32 getParamIndex(StringView name) const {
		for (const Variable& var : m_globals) {
			if (equalStrings(var.name, name)) return i32(&var - m_globals.begin());
		}
		return -1;
	}

	static i32 find(Span<const Variable> vars, StringView name) {
		for (const Variable& var : vars) {
			if (equalStrings(var.name, name)) return i32(&var - vars.begin());
		}
		return -1;
	}

	Constant* getConstant(StringView name) const {
        for (Constant& c : m_constants) {
            if (equalStrings(c.name, name)) return &c;
        }
        return nullptr;
    }

	i32 getLine(StringView location) {
        ASSERT(location.begin <= m_tokenizer.m_document.end);
        const char* c = m_tokenizer.m_document.begin;
        i32 line = 1;
        while(c < location.begin) {
            if (*c == '\n') ++line;
            ++c;
        }
        return line;
    }

	template <typename... Args>
	void error(StringView location, Args&&... args) {
		if(!m_is_error) logError(m_path, "(", getLine(location), "): ", args...);
		m_is_error = true;
	}

	template <typename... Args>
	void errorAtCurrent(Args&&... args) {
		if(!m_is_error) logError(m_path, "(", getLine(m_tokenizer.m_current_token.value), "): ", args...);
		m_is_error = true;
	}

	Token peekToken() { return m_tokenizer.m_current_token; }

	Token consumeToken() {
		Token t = m_tokenizer.m_current_token;
		m_tokenizer.m_current_token = m_tokenizer.nextToken();
		return t;
	}

const char* toString(Token::Type type) {
		switch (type) {
			case Token::Type::COLON: return ";";
			case Token::Type::COMMA: return ",";
			case Token::Type::CONST: return "const";
			case Token::Type::DOT: return ".";
			case Token::Type::EMITTER: return "emitter";
			case Token::Type::EOF: return "end of file";
			case Token::Type::ERROR: return "error";
			case Token::Type::SEMICOLON: return ";";
			case Token::Type::LEFT_PAREN: return "(";
			case Token::Type::RIGHT_PAREN: return ")";
			case Token::Type::LEFT_BRACE: return "{";
			case Token::Type::RIGHT_BRACE: return "}";
			case Token::Type::STAR: return "*";
			case Token::Type::SLASH: return "/";
			case Token::Type::MINUS: return "-";
			case Token::Type::PLUS: return "+";
			case Token::Type::EQUAL: return "=";
			case Token::Type::PERCENT: return "%";
			case Token::Type::GT: return ">";
			case Token::Type::LT: return "<";
			case Token::Type::NUMBER: return "number";
			case Token::Type::STRING: return "string";
			case Token::Type::IDENTIFIER: return "identifier";
			case Token::Type::GLOBAL: return "global";
			case Token::Type::FN: return "fn";
			case Token::Type::VAR: return "var";
			case Token::Type::OUT: return "out";
			case Token::Type::IN: return "in";
			case Token::Type::LET: return "let";
			case Token::Type::RETURN: return "return";
			case Token::Type::IMPORT: return "import";
			case Token::Type::IF: return "if";
			case Token::Type::ELSE: return "else";
		}
		ASSERT(false);
		return "N//A";
	}

	bool consume(Token::Type type) {
		Token t = consumeToken();
		if (t.type != type) {
			error(t.value, "Missing ", toString(type), " before ", t.value);
			return false;
		}
		return true;
	}

	[[nodiscard]] bool consume(Token::Type type, StringView& value) {
		Token t = consumeToken();
		if (t.type != type) {
			error(t.value, "Missing ", toString(type), " before ", t.value);
			return false;
		}
		value = t.value;
		return true;
	}	

	float asFloat(Token token) {
		ASSERT(token.type == Token::NUMBER);
		float v;
		fromCString(token.value, v);
		return v;
	}

	static u32 getPriority(const Token& token) {
		switch (token.type) {
			case Token::GT: case Token::LT: return 1;
			case Token::PLUS: case Token::MINUS: return 2;
			case Token::PERCENT: case Token::STAR: case Token::SLASH: return 3;
			default: ASSERT(false);
		}
		return 0;
	}

	i32 toCount(ValueType type) {
		switch (type) {
			case ValueType::VOID: return 0;
			case ValueType::FLOAT: return 1;
			case ValueType::FLOAT3: return 3;
			case ValueType::FLOAT4: return 4;
		}
		ASSERT(false);
		return -1;
	}

	template <typename F>
	void forEachSwizzleToIndex(StringView swizzle, F&& f) {
		for (u32 i = 0, c = swizzle.size(); i < c; ++i) {	
			switch (swizzle[i]) {
				case 'x': case 'r': f(0); break;
				case 'y': case 'g': f(1); break;
				case 'z': case 'b': f(2); break;
				case 'w': case 'a': f(3); break;
			}
		}
	}

	// original nodes are left in undefined state
	Node* collapseConstants(Node* node, BlockNode* block, bool is_conditional_block) {
		switch (node->type) {
			case Node::RETURN: {
				auto* r = (ReturnNode*)node;
				r->value = collapseConstants(r->value, block, is_conditional_block);
				return r;
			}
			case Node::ASSIGN: {
				auto* n = (AssignNode*)node;
				n->right = collapseConstants(n->right, block, is_conditional_block);
				
				// n->left is either variable of swizzle node
				// if n->right is not const, we set the local's is_const to false
				// if n->right is const, we set the local's is_const to true and set local's values
				
				if (n->left->type != Node::VARIABLE && n->left->type != Node::SWIZZLE) return node;

				VariableNode* lhs_var;
				StringView swizzle;
				if (n->left->type == Node::SWIZZLE) {
					auto* swizzle_node = (SwizzleNode*)n->left;
					if (swizzle_node->left->type != Node::VARIABLE) return node;

					swizzle = swizzle_node->token.value;
					lhs_var = (VariableNode*)swizzle_node->left;
					// only locals can be collapsed to constant
					if (lhs_var->family != VariableFamily::LOCAL) return node;
				}
				else {
					lhs_var = (VariableNode*)n->left;
					if (lhs_var->family != VariableFamily::LOCAL) return node;

					switch (lhs_var->block->locals[lhs_var->index].type) {
						case ValueType::FLOAT: swizzle = "x"; break;
						case ValueType::FLOAT3: swizzle = "xyz"; break;
						case ValueType::FLOAT4: swizzle = "xyzw"; break;
						case ValueType::VOID: ASSERT(false); swizzle = ""; break;
					}
				}

				Local& local = lhs_var->block->locals[lhs_var->index];
				// we assign to outer scope local inside if/else
				// so make the local non-const
				if (lhs_var->block != block && is_conditional_block) {
					forEachSwizzleToIndex(swizzle, [&](i32 idx) {
						local.is_const[idx] = false;
					});
					return node;
				}

				if (n->right->type == Node::LITERAL) {
					forEachSwizzleToIndex(swizzle, [&](i32 idx) {
						local.is_const[idx] = true;
						local.values[idx] = ((LiteralNode*)n->right)->value;
					});
					return node;
				}
				
				if (n->right->type == Node::COMPOUND) {
					auto* cn = (CompoundNode*)n->right;
					ASSERT(cn->elements.size() <= 4);
					forEachSwizzleToIndex(swizzle, [&](i32 idx) {
						if (cn->elements[idx]->type == Node::LITERAL) {
							local.is_const[idx] = true;
							local.values[idx] = ((LiteralNode*)cn->elements[idx])->value;
						}
						else {
							local.is_const[idx] = false;
						}
					});
					return node;
				}

				forEachSwizzleToIndex(swizzle, [&](i32 idx) {
					local.is_const[idx] = false;
				});
				return node;
			}
			case Node::SWIZZLE: {
				auto* n = (SwizzleNode*)node;
				n->left = collapseConstants(n->left, block, is_conditional_block);
				switch (n->left->type) {
					case Node::LITERAL: return n->left;
					case Node::COMPOUND: {
						auto* compound = (CompoundNode*)n->left;
						if (n->token.value.size() == 1) {
							switch (n->token.value[0]) {
								case 'x': case 'r': return compound->elements[0];
								case 'y': case 'g': return compound->elements[1];
								case 'z': case 'b': return compound->elements[2];
								case 'w': case 'a': return compound->elements[3];
								default: ASSERT(false); return node;
							}
						}

						auto* swizzled = LUMIX_NEW(m_arena_allocator, CompoundNode)(n->token, m_arena_allocator);
						swizzled->elements.reserve(n->token.value.size());
						forEachSwizzleToIndex(n->token.value, [&](i32 idx){
							swizzled->elements.push(compound->elements[idx]);
						});
						return swizzled;
					}
					default: return node;
				}
			}
			case Node::COMPOUND: {
				auto* n = (CompoundNode*)node;
				for (Node*& element : n->elements) {
					element = collapseConstants(element, block, is_conditional_block);
				}
				return node;
			}
			case Node::BLOCK: {
				auto* n = (BlockNode*)node;
				for (Node*& statement : n->statements) {
					statement = collapseConstants(statement, n, is_conditional_block);
				}
				n->statements.eraseItems([](Node* n){ return n == nullptr; });
				return node;
			}
			case Node::SYSCALL: {
				auto* n = (SysCallNode*)node;
				bool all_args_const = n->args.size() > 0;
				for (Node*& arg : n->args) {
					arg = collapseConstants(arg, block, is_conditional_block);
					if (arg->type != Node::LITERAL) all_args_const = false;
				}
				if (all_args_const) {
					auto* arg0 = ((LiteralNode*)n->args[0]);
					auto* arg1 = n->args.size() > 1 ? ((LiteralNode*)n->args[1]) : nullptr;
					switch (n->function.instruction) {
						case InstructionType::COS: {
							auto* res = LUMIX_NEW(m_arena_allocator, LiteralNode)(n->token);
							res->value = cosf(arg0->value);
							return res;
						}
						case InstructionType::SIN: {
							auto* res = LUMIX_NEW(m_arena_allocator, LiteralNode)(n->token);
							res->value = sinf(arg0->value);
							return res;
						}
						case InstructionType::SQRT: {
							auto* res = LUMIX_NEW(m_arena_allocator, LiteralNode)(n->token);
							res->value = sqrtf(arg0->value);
							return res;
						}
						case InstructionType::MIN: {
							auto* res = LUMIX_NEW(m_arena_allocator, LiteralNode)(n->token);
							res->value = minimum(arg0->value, arg1->value);
							return res;
						}
						case InstructionType::MAX: {
							auto* res = LUMIX_NEW(m_arena_allocator, LiteralNode)(n->token);
							res->value = maximum(arg0->value, arg1->value);
							return res;
						}
						default: return node;
					}
				}
				return node;
			}
			case Node::IF: {
				auto* n = (IfNode*)node;
				n->condition = collapseConstants(n->condition, block, is_conditional_block);
				if (n->condition->type == Node::LITERAL) {
					float v = ((LiteralNode*)n->condition)->value;
					if (v == 0) {
						if (n->false_block) collapseConstants(n->false_block, block, false);
						return n->false_block;
					}
					collapseConstants(n->true_block, block, false);
					return n->true_block;
				}
				
				collapseConstants(n->true_block, block, true);
				if (n->false_block) collapseConstants(n->false_block, block, true);
				return node;
			}
			case Node::FUNCTION_CALL: {
				auto* n = (FunctionCallNode*)node;
				for (Node*& arg : n->args) arg = collapseConstants(arg, block, is_conditional_block);
				return node;
			}
			case Node::FUNCTION_ARG: return node;
			case Node::EMITTER_REF: return node;
			case Node::SYSTEM_VALUE: return node;
			case Node::VARIABLE: {
				auto* n = (VariableNode*)node;
				if (n->family != VariableFamily::LOCAL) return node;

				const Local& local = n->block->locals[n->index];
				if (local.type == ValueType::FLOAT) {
					if (local.is_const[0]) {
						LiteralNode* ln = LUMIX_NEW(m_arena_allocator, LiteralNode)(node->token);
						ln->value = local.values[0];
						return ln;
					}
				}

				u32 num = toCount(local.type);
				for (u32 i = 0; i < num; ++i) {
					if (!local.is_const[i]) return node;
				}
				CompoundNode* cn = LUMIX_NEW(m_arena_allocator, CompoundNode)(node->token, m_arena_allocator);
				cn->elements.reserve(num);
				for (u32 i = 0; i < num; ++i) {
					LiteralNode* ln = LUMIX_NEW(m_arena_allocator, LiteralNode)(node->token);
					ln->value = local.values[i];
					cn->elements.push(ln);
				}
				return cn;
			}
			case Node::LITERAL: return node;
			case Node::BINARY_OPERATOR: {
				auto* n = (BinaryOperatorNode*)node;
				n->left = collapseConstants(n->left, block, is_conditional_block); 
				n->right = collapseConstants(n->right, block, is_conditional_block);

				if (n->left->type == Node::LITERAL) {
					auto* l = (LiteralNode*)n->left;
					switch (n->op) {
						case Operators::ADD: if (l->value == 0) return n->right; break;
						case Operators::MUL:
							if (l->value == 1) return n->right;
							if (l->value == 0) return l;
							break;
						default: break;
					}
				}

				if (n->right->type == Node::LITERAL) {
					auto* r = (LiteralNode*)n->right;
					switch (n->op) {
						case Operators::ADD: if (r->value == 0) return n->left; break;
						case Operators::MUL:
							if (r->value == 1) return n->left;
							if (r->value == 0) return r;
							break;
						default: break;
					}
				}

				if (n->left->type != Node::LITERAL || n->right->type != Node::LITERAL) return node;

				auto* r = (LiteralNode*)n->right;
				auto* l = (LiteralNode*)n->left;
				switch (n->op) {
					case Operators::ADD: l->value = l->value + r->value; return l;
					case Operators::SUB: l->value = l->value - r->value; return l;
					case Operators::MUL: l->value = l->value * r->value; return l;
					case Operators::DIV: l->value = l->value / r->value; return l;
					case Operators::MOD: l->value = fmodf(l->value, r->value); return l;
					case Operators::LT: l->value = l->value < r->value ? 1.f : 0.f; return l;
					case Operators::GT: l->value = l->value > r->value ? 1.f : 0.f; return l;
					default: return node;
				}
			}
			case Node::UNARY_OPERATOR: {
				auto* n = (UnaryOperatorNode*)node;
				if (n->op != Operators::SUB) return node;
				
				Node* r = collapseConstants(n->right, block, is_conditional_block);
				if (r->type == Node::LITERAL) {
					auto* literal = (LiteralNode*)r;
					literal->value = -literal->value;
					return literal;
				}
				return node;
			}
		}
		return node;
	}

	Node* atom(CompileContext& ctx) {
		Node* left = atomInternal(ctx);
		if (peekToken().type != Token::DOT) return left;
		// swizzle
		consumeToken();
		Token swizzle = consumeToken();

		if (swizzle.type != Token::IDENTIFIER) {
			error(swizzle.value, "Invalid swizzle ", swizzle.value);
			return nullptr;
		}
		auto* node = LUMIX_NEW(m_arena_allocator, SwizzleNode)(swizzle);
		node->left = left;
		return node;
	}
	
	static bool tokenMatchRemaining(StringView name, const char* remaining, i32 start, i32 len) {
		if (name.size() != start + len) return false;
		if (memcmp(name.begin + start, remaining, len) == 0) return true;
		return false;
	}

	ParticleSystemValues getSystemValue(StringView name) {
		switch (name[0]) {
			case 'e':
				if (tokenMatchRemaining(name, "mit_index", 1, 9)) return ParticleSystemValues::EMIT_INDEX;
				break; 
			case 'r':
				if (tokenMatchRemaining(name, "ibbon_index", 1, 11)) return ParticleSystemValues::RIBBON_INDEX;
				break; 
			case 't': 
				if (name.size() < 2) return ParticleSystemValues::NONE;
				switch (name[1]) {
					case 'i':
						if (tokenMatchRemaining(name, "me_delta", 2, 8)) return ParticleSystemValues::TIME_DELTA;
						break; 
					case 'o':
						if (tokenMatchRemaining(name, "tal_time", 2, 8)) return ParticleSystemValues::TOTAL_TIME;
						break; 
				}
				break;
		}
		return ParticleSystemValues::NONE;
	}

	SysCall checkBuiltinFunction(StringView name, const char* remaining, i32 start, i32 len, SysCall if_matching) {
		if (name.size() != start + len) return {InstructionType::END};
		name.removePrefix(start);
		if (memcmp(name.begin, remaining, len) == 0) return if_matching;
		return {InstructionType::END};
	}

	SysCall getSysCall(StringView name) {
		switch (name[0]) {
			case 'c': 
				if (name.size() < 2) return {InstructionType::END};
				switch (name[1]) {
					case 'o': return checkBuiltinFunction(name, "s", 2, 1, { InstructionType::COS, true, 1 });
					case 'u': return checkBuiltinFunction(name, "rve", 2, 3, { InstructionType::GRADIENT, true, 0xff });
				}
				return { InstructionType::END };
				
			case 'e': return checkBuiltinFunction(name, "mit", 1, 3, { InstructionType::EMIT, false, 1, 1 << u32(EntryPoint::UPDATE) });
			case 'k': return checkBuiltinFunction(name, "ill", 1, 3, { InstructionType::KILL, false, 0, 1 << u32(EntryPoint::UPDATE) });
			case 'm':
				if (name.size() < 2) return {InstructionType::END};
				switch (name[1]) {
					case 'a': return checkBuiltinFunction(name, "x", 2, 1, { InstructionType::MAX, true, 2 });
					case 'e': return checkBuiltinFunction(name, "sh", 2, 2, { InstructionType::MESH, true, 0 });
					case 'i': return checkBuiltinFunction(name, "n", 2, 1, { InstructionType::MIN, true, 2 });
				}
				return { InstructionType::END };

			case 'n': return checkBuiltinFunction(name, "oise", 1, 4, { InstructionType::NOISE, true, 1 });
			case 'r': return checkBuiltinFunction(name, "andom", 1, 5, { InstructionType::RAND, true, 2 });
			case 's': 
				if (name.size() < 2) return {InstructionType::END};
				switch (name[1]) {
					case 'i': return checkBuiltinFunction(name, "n", 2, 1, { InstructionType::SIN, true, 1 });
					case 'q': return checkBuiltinFunction(name, "rt", 2, 2, { InstructionType::SQRT, true, 1 });
				}
				return { InstructionType::END };
				
		}
		return { InstructionType::END };
	}

	BlockNode* block(CompileContext& ctx) {
		auto* node = LUMIX_NEW(m_arena_allocator, BlockNode)(peekToken(), m_arena_allocator);
		node->parent = ctx.block;
		ctx.block = node;
		defer { ctx.block = node->parent; };
		if (!consume(Token::LEFT_BRACE)) return nullptr;
		node->statements.reserve(8);
		for (;;) {
			Token token = peekToken();
			switch (token.type) {
				case Token::ERROR: return nullptr;
				case Token::EOF:
					errorAtCurrent("Unexpected end of file.");
					return nullptr;
				case Token::LEFT_BRACE: {
					Node* s = block(ctx);
					if (!s) return nullptr;
					node->statements.push(s);
				}
				case Token::LET:
					declareLocal(ctx);
					break;
				case Token::RIGHT_BRACE:
					consumeToken();
					return node;
				default: {
					Node* s = statement(ctx);
					if (!s) return nullptr;
					node->statements.push(s);
					break;
				}
			}
		}
	}

	Node* atomInternal(CompileContext& ctx) {
		Token token = consumeToken();
		switch (token.type) {
			case Token::EOF:
				error(token.value, "Unexpected end of file.");
				return nullptr;
			case Token::ERROR: return nullptr;
			case Token::LEFT_BRACE: {
				auto* node = LUMIX_NEW(m_arena_allocator, CompoundNode)(token, m_arena_allocator);
				node->elements.reserve(4);
				for (;;) {
					Token t = peekToken();
					switch (t.type) {
						case Token::ERROR: return nullptr;
						case Token::EOF:
							errorAtCurrent("Unexpected end of file.");
							return nullptr;
						case Token::RIGHT_BRACE:
							consumeToken();
							return node;
						default: {
							if (!node->elements.empty() && !consume(Token::COMMA)) return nullptr;
							Node* element = expression(ctx, 0);
							if (!element) return nullptr;
							node->elements.push(element);
							break;
						}
					}
				}
			}
			case Token::LEFT_PAREN: {
				Node* res = expression(ctx, 0);
				if (!consume(Token::RIGHT_PAREN)) return nullptr;
				return res;
			}
			case Token::IDENTIFIER: {
				i32 param_index = getParamIndex(token.value);
				if (param_index >= 0) {
					auto* node = LUMIX_NEW(m_arena_allocator, VariableNode)(token);
					node->family = VariableFamily::GLOBAL;
					node->index = param_index;
					return node;
				}

				for (Emitter& e : m_emitters) {
					if (equalStrings(e.m_name, token.value)) {
						auto* node = LUMIX_NEW(m_arena_allocator, EmitterRefNode)(token);
						node->index = i32(&e - m_emitters.begin());
						return node;
					}
				}

				if (equalStrings(token.value, "entity_position")) {
					auto* node = LUMIX_NEW(m_arena_allocator, CompoundNode)(token, m_arena_allocator);
					node->elements.reserve(3);
					auto* x = LUMIX_NEW(m_arena_allocator, SystemValueNode)(token);
					x->value = ParticleSystemValues::ENTITY_POSITION_X;
					node->elements.push(x);
					
					auto* y = LUMIX_NEW(m_arena_allocator, SystemValueNode)(token);
					y->value = ParticleSystemValues::ENTITY_POSITION_Y;
					node->elements.push(y);
					
					auto* z = LUMIX_NEW(m_arena_allocator, SystemValueNode)(token);
					z->value = ParticleSystemValues::ENTITY_POSITION_Z;
					node->elements.push(z);
					return node;
				}

				ParticleSystemValues system_value = getSystemValue(token.value);
				if (system_value != ParticleSystemValues::NONE) {
					auto* node = LUMIX_NEW(m_arena_allocator, SystemValueNode)(token);
					node->value = system_value;
					return node;
				}

				SysCall syscall = getSysCall(token.value);
				if (syscall.instruction != InstructionType::END) {
					auto* node = LUMIX_NEW(m_arena_allocator, SysCallNode)(token, m_arena_allocator);
					if (!consume(Token::LEFT_PAREN)) return nullptr;
					node->args.reserve(syscall.num_args);
					for (u32 i = 0; i < syscall.num_args; ++i) {
						if (i > 0 && !consume(Token::COMMA)) return nullptr;
						Node* arg = expression(ctx, 0);
						if (!arg) return nullptr;
						node->args.push(arg);
					}
					if (!consume(Token::RIGHT_PAREN)) return nullptr;
		
					if (peekToken().type == Token::LEFT_BRACE) {
						CompileContext inner_ctx = ctx;
						if (syscall.instruction == InstructionType::EMIT) {
							if (node->args[0]->type != Node::EMITTER_REF) {
								error(node->args[0]->token.value, "First parameter must an emitter.");
								return nullptr;
							}
							u32 emitter_index = ((EmitterRefNode*)node->args[0])->index;
							inner_ctx.emitted = &m_emitters[emitter_index];
						}
						node->after_block = block(inner_ctx);
						if (!node->after_block) return nullptr;
					}

					node->function = syscall;
					return node;
				}

				if (ctx.emitted) {
					i32 input_index = find(ctx.emitted->m_inputs, token.value);
					if (input_index >= 0) {
						auto* node = LUMIX_NEW(m_arena_allocator, VariableNode)(token);
						node->family = VariableFamily::INPUT;
						node->index = input_index;
						return node;
					}
				}

				if (ctx.emitter) {
					i32 output_index = find(ctx.emitter->m_outputs, token.value);
					if (output_index >= 0) {
						auto* node = LUMIX_NEW(m_arena_allocator, VariableNode)(token);
						node->family = VariableFamily::OUTPUT;
						node->index = output_index;
						return node;
					}

					i32 input_index = find(ctx.emitter->m_inputs, token.value);
					if (input_index >= 0) {
						auto* node = LUMIX_NEW(m_arena_allocator, VariableNode)(token);
						node->family = VariableFamily::INPUT;
						node->index = input_index;
						return node;
					}

					i32 var_index = find(ctx.emitter->m_vars, token.value);
					if (var_index >= 0) {
						auto* node = LUMIX_NEW(m_arena_allocator, VariableNode)(token);
						node->family = VariableFamily::CHANNEL;
						node->index = var_index;
						return node;
					}
				}

				i32 fn_index = getFunctionIndex(token.value);
				if (fn_index >= 0) {
					auto* node = LUMIX_NEW(m_arena_allocator, FunctionCallNode)(token, m_arena_allocator);
					if (!consume(Token::LEFT_PAREN)) return nullptr;
					node->function_index = fn_index;
					const Function& fn = m_functions[fn_index];
					for (u32 i = 0, c = fn.args.size(); i < c; ++i) {
						if (i > 0 && !consume(Token::COMMA)) return nullptr;
						Node* arg = expression(ctx, 0);
						if (!arg) return nullptr;
						node->args.push(arg);
					}
					if (!consume(Token::RIGHT_PAREN)) return nullptr;
					return node;
				}

				if (ctx.function) {
					i32 arg_index = getArgumentIndex(*ctx.function, token.value);
					if (arg_index >= 0) {
						auto* node = LUMIX_NEW(m_arena_allocator, FunctionArgNode)(token);
						node->index = arg_index;
						return node;
					}
				}

				Constant* c = getConstant(token.value);
				if (c) {
					auto* node = LUMIX_NEW(m_arena_allocator, LiteralNode)(token);
					// TODO floatN
					node->value = c->value[0];
					return node;
				}
				
				BlockNode* block = ctx.block;
				while (block) {
					for (const Local& local : block->locals) {
						if (equalStrings(local.name, token.value)) {
							auto* node = LUMIX_NEW(m_arena_allocator, VariableNode)(token);
							node->family = VariableFamily::LOCAL;
							node->block = block;
							node->index = i32(&local - block->locals.begin());
							return node;
						}
					}
					block = block->parent;
				}

				error(token.value, "Unexpected token ", token.value);
				return nullptr;
			}

			case Token::MINUS: {
				UnaryOperatorNode* node = LUMIX_NEW(m_arena_allocator, UnaryOperatorNode)(token);
				node->op = (Operators)token.value[0];
				node->right = atom(ctx);
				if (!node->right) return nullptr;
				return node;
			}
			case Token::NUMBER: {
				LiteralNode* node = LUMIX_NEW(m_arena_allocator, LiteralNode)(token);
				node->value = asFloat(token);
				return node;
			}
			default:
				error(token.value, "Unexpected token ", token.value);
				return nullptr;
		}
		return nullptr;
	} 

	// let a = ...;
	// let a : type;
	// let a : type = ...;
	void declareLocal(CompileContext& ctx) {
		if (!consume(Token::LET)) return;
		BlockNode* block = ctx.block;
		Local& local = block->locals.emplace();
		if (!consume(Token::IDENTIFIER, local.name)) return;

		bool infer_type = false;
		if (peekToken().type == Token::COLON) {
			// let a : type ...
			if (!consume(Token::COLON)) return;
			local.type = parseType();
		}
		else if (peekToken().type == Token::EQUAL) {
			// let a = ...;
			infer_type = true;
			local.type = ValueType::FLOAT;
		}
		else {
			error(peekToken().value, "Unexpected token ", peekToken().value);
			return;
		}

		if (peekToken().type == Token::SEMICOLON) {
			// let a : type;
			consumeToken();
			return;
		}
		Token equal_token = peekToken();
		if (!consume(Token::EQUAL)) return;

		// let a : type = ...
		// let a = ...
		Node* value = expression(ctx, 0);
		if (!value) return;

		if (value->type == Node::COMPOUND && infer_type) {
			switch (((CompoundNode*)value)->elements.size()) {
				case 1: local.type = ValueType::FLOAT; break;
				case 3: local.type = ValueType::FLOAT3; break;
				case 4: local.type = ValueType::FLOAT4; break;
				default: ASSERT(false); break;
			}
		}

		auto* assign = LUMIX_NEW(m_arena_allocator, AssignNode)(equal_token);
		assign->right = value;
		auto* var_node = LUMIX_NEW(m_arena_allocator, VariableNode)(equal_token);
		var_node->family = VariableFamily::LOCAL;
		var_node->block = ctx.block;
		var_node->index = ctx.block->locals.size() - 1;
		assign->left = var_node;
		ctx.block->statements.push(assign);

		consume(Token::SEMICOLON);
	}

	Node* ifStatement(CompileContext& ctx) {
		IfNode* result = LUMIX_NEW(m_arena_allocator, IfNode)(peekToken());
		result->condition = expression(ctx, 0);
		if (!result->condition) return nullptr;

		result->true_block = block(ctx);
		if (!result->true_block) return nullptr;

		if (peekToken().type == Token::ELSE) {
			consumeToken();
			result->false_block = block(ctx);
			if (!result->false_block) return nullptr;
		}
		return result;
	}

	Node* statement(CompileContext& ctx) {
		Token token = peekToken();
		switch (token.type) {
			case Token::IF: 
				consumeToken();
				return ifStatement(ctx);
			case Token::IDENTIFIER: {
				Node* lhs = atom(ctx);
				if (!lhs) return nullptr;

				Token op = peekToken();
				switch (op.type) {
					case Token::SEMICOLON: 
						consumeToken();
						return lhs;
					case Token::EQUAL: {
						consumeToken();
						Node* value = expression(ctx, 0);
						if (!value) return nullptr;
						auto* node = LUMIX_NEW(m_arena_allocator, AssignNode)(op);
						node->left = lhs;
						node->right = value;
						if (!consume(Token::SEMICOLON)) return nullptr;
						return node;
					}
					default:
						error(op.value, "Unexpected token ", op.value);
						return nullptr;
				}
			}
			case Token::RETURN: {
				consumeToken();
				auto* node = LUMIX_NEW(m_arena_allocator, ReturnNode)(token);
				node->value = expression(ctx, 0);
				if (!node->value) return nullptr;
				if (!consume(Token::SEMICOLON)) return nullptr;
				return node;
			}
			default:
				errorAtCurrent("Unexpected token ", token.value);
				return nullptr;
		}
	}

	// does not consume terminating token
	Node* expression(CompileContext& ctx, u32 min_priority) {
		Node* lhs = atom(ctx);
		if (!lhs) return nullptr;
		
		for (;;) {
			Token op = peekToken();
			switch (op.type) {
				case Token::EOF: return lhs;
				case Token::ERROR: return nullptr;

				case Token::PERCENT:
				case Token::LT: case Token::GT:
				case Token::SLASH: case Token::STAR:
				case Token::MINUS: case Token::PLUS: {
					u32 prio = getPriority(op);
					if (prio <= min_priority) return lhs;
					consumeToken();
					Node* rhs = expression(ctx, prio);
					BinaryOperatorNode* opnode = LUMIX_NEW(m_arena_allocator, BinaryOperatorNode)(op);
					opnode->op = (Operators)op.value[0];
					opnode->left = lhs;
					opnode->right = rhs;
					lhs = opnode;
					break;
				}

				default: return lhs;
			}
		}
	}

	Array<OutputMemoryStream> m_imports;

	void parseImport() {
		StringView path;
		if (!consume(Token::STRING, path)) return;
		
		OutputMemoryStream& import_content = m_imports.emplace(m_allocator);
		if (!m_app.getEngine().getFileSystem().getContentSync(Path(path), import_content)) {
			error(path, "Failed to load import ", path);
			return;
		}
		
		ParticleScriptTokenizer tokenizer = m_tokenizer;
		m_tokenizer.m_current = (const char*)import_content.data();
		m_tokenizer.m_document = StringView((const char*)import_content.data(), (u32)import_content.size());
		m_tokenizer.m_current_token = m_tokenizer.nextToken();
		
		for (;;) {
			Token token = consumeToken();
			switch (token.type) {
				case Token::EOF: goto end_import;
				case Token::ERROR: return;
				case Token::IMPORT: parseImport(); break;
				case Token::CONST: parseConst(); break;
				case Token::FN: parseFunction(); break;
				case Token::GLOBAL: variableDeclaration(m_globals); break;
				case Token::EMITTER: compileEmitter(); break;
				default: error(token.value, "Unexpected token ", token.value); return;
			}
		}
		end_import:
		
		m_tokenizer = tokenizer;
	}

	void parseConst() {
        Constant& c = m_constants.emplace();
		if (!consume(Token::IDENTIFIER, c.name)) return;
        if (!consume(Token::EQUAL)) return;
        
		CompileContext ctx(*this);
		ctx.entry_point = EntryPoint::GLOBAL;
		Node* n = expression(ctx, 0);
		if (!n) return;
		n = collapseConstants(n, nullptr, false);

		if (n->type != Node::LITERAL) {
			// TODO floatN constants
			errorAtCurrent("Expected a constant.");
			return;
		}

        float f = ((LiteralNode*)n)->value;
        c.value[0] = f;
        c.value[1] = f;
        c.value[2] = f;
        c.value[3] = f;
        c.type = ValueType::FLOAT;

		consume(Token::SEMICOLON);
	}

	void parseArgs(Function& fn) {
		consume(Token::LEFT_PAREN);
		bool comma = false;
		for (;;) {
			Token t = consumeToken();
			switch (t.type) {
				case Token::ERROR: return;
				case Token::EOF: error(t.value, "Unexpected end of file.");
				case Token::RIGHT_PAREN:
					if (!comma) return;
					error(t.value, "Unexpected ).'");
					return;
				case Token::COMMA: 
					if (fn.args.empty() || comma) {
						error(t.value, "Unexpected ,.");
						return;
					}
					comma = true;
					break;
				case Token::IDENTIFIER: {
					StringView& arg = fn.args.emplace();
					arg = t.value;
					comma = false;
					break;
				}
				default:
					error(t.value, "Unexpected token ", t.value);
					return;
			}
		}
	}

	u32 forEachDataStreamInBytecode(InputMemoryStream& ip, auto f, u32 instruction_index_offset = 0) {
		u32 instruction_index = instruction_index_offset;
		auto forNumStreams = [&](u32 num, InstructionType itype, i32 ioffset) {
			for (u32 i = 0; i < num; ++i) {
				i32 pos = (i32)ip.getPosition();
				DataStream dst = ip.read<DataStream>();
				f(dst, pos, i, itype, ioffset, instruction_index);
			}
			++instruction_index;
		};
		for(;;) {
			i32 ioffset = (i32)ip.getPosition();
			InstructionType itype = ip.read<InstructionType>();
			switch (itype) {
				case InstructionType::END: return instruction_index;
				case InstructionType::CMP_ELSE:
					forNumStreams(1, itype, ioffset);
					ip.skip(sizeof(u16) * 2); // blocks' sizes
					instruction_index = forEachDataStreamInBytecode(ip, f, instruction_index); // if subblock
					instruction_index = forEachDataStreamInBytecode(ip, f, instruction_index); // else subblock
					break;
				case InstructionType::CMP: 
					forNumStreams(1, itype, ioffset);
					ip.skip(sizeof(u16)); // block size
					instruction_index = forEachDataStreamInBytecode(ip, f, instruction_index); // if subblock
					break;
				case InstructionType::NOISE: forNumStreams(2, itype, ioffset); break;
				case InstructionType::MOV: forNumStreams(2, itype, ioffset); break;
				case InstructionType::SIN: forNumStreams(2, itype, ioffset); break;
				case InstructionType::COS: forNumStreams(2, itype, ioffset); break;
				case InstructionType::SQRT: forNumStreams(2, itype, ioffset); break;
				case InstructionType::GT: forNumStreams(3, itype, ioffset); break;
				case InstructionType::LT: forNumStreams(3, itype, ioffset); break;
				case InstructionType::SUB: forNumStreams(3, itype, ioffset); break;
				case InstructionType::ADD: forNumStreams(3, itype, ioffset); break;
				case InstructionType::MUL: forNumStreams(3, itype, ioffset); break;
				case InstructionType::DIV: forNumStreams(3, itype, ioffset); break;
				case InstructionType::MOD: forNumStreams(3, itype, ioffset); break;
				case InstructionType::AND: forNumStreams(3, itype, ioffset); break;
				case InstructionType::OR: forNumStreams(3, itype, ioffset); break;
				case InstructionType::MIN: forNumStreams(3, itype, ioffset); break;
				case InstructionType::MAX: forNumStreams(3, itype, ioffset); break;
				case InstructionType::KILL: break;
				case InstructionType::RAND: forNumStreams(1, itype, ioffset); ip.skip(sizeof(float) * 2); break;
				case InstructionType::EMIT:
						ip.skip(sizeof(u32)); // emitter index
						instruction_index = forEachDataStreamInBytecode(ip, f, instruction_index); // emit subroutine
						break;
				default:
					ASSERT(false);
					break;
			}
		}
	}

	// some instructions do jumps relative to them, let's compute the jump size after all optimizations are done
	void patchBlockSizes(OutputMemoryStream& bytecode) {
		InputMemoryStream ip(bytecode);
		forEachDataStreamInBytecode(ip, [&](const DataStream& s, i32 position, i32 arg_index, InstructionType itype, i32 ioffset, i32){
			if (arg_index != 0) return;
			switch (itype) {
				case InstructionType::CMP_ELSE: {
					u64 offset = position + sizeof(DataStream);
					
					InputMemoryStream inner((const u8*)ip.getData() + offset + sizeof(u16) * 2, ip.size() - offset - sizeof(u16) * 2);
					forEachDataStreamInBytecode(inner, [](const DataStream& s, i32 position, i32 arg_index, InstructionType itype, i32 ioffset, i32){});
					u64 true_size = inner.getPosition();
					forEachDataStreamInBytecode(inner, [](const DataStream& s, i32 position, i32 arg_index, InstructionType itype, i32 ioffset, i32){});
					u64 false_size = inner.getPosition() - true_size;
					*(u16*)(bytecode.getMutableData() + offset) = u16(true_size);
					*(u16*)(bytecode.getMutableData() + offset + 2) = u16(false_size);

					break;
				}
				case InstructionType::CMP: {
					u64 offset = position + sizeof(DataStream);
					
					InputMemoryStream inner((const u8*)ip.getData() + offset + sizeof(u16), ip.size() - offset - sizeof(u16));
					forEachDataStreamInBytecode(inner, [](const DataStream& s, i32 position, i32 arg_index, InstructionType itype, i32 ioffset, i32){});
					u16 size = (u16)inner.getPosition();
					
					memcpy(bytecode.getMutableData() + offset, &size, sizeof(size));
					break;
				}
				default: break;
			}
		});
	}

	struct IRNode {
		#ifdef LUMIX_DEBUG
			// so VS debugger can show use actual type
			virtual ~IRNode() {}
		#endif
		enum Type {
			OP, // mov, binary op or syscall
			IF,
			END
		};

		IRNode(Type type, Node* ast) : type(type), ast(ast) {}

		Type type;
		IRNode* next = nullptr;
		IRNode* prev = nullptr;
		Node* ast = nullptr; 
	};

	struct IRValue {
		DataStream::Type type;
		u32 index;
		float value;

		bool operator==(const IRValue& rhs) const {
			if (type != rhs.type) return false;
			if (type == DataStream::LITERAL) return value == rhs.value;
			return index == rhs.index;
		}
	};

	struct IROp : IRNode {
		IROp(Node* ast, IAllocator& allocator) : IRNode(IRNode::OP, ast), args(allocator) {}
		InstructionType instruction;
		IRValue dst;
		StackArray<IRValue, 2> args;
	};

	struct IREnd : IRNode {
		IREnd(Node* ast) : IRNode(IRNode::END, ast) {}
	};

	struct IRIf : IRNode {
		IRIf(Node* ast) : IRNode(IRNode::IF, ast) {}
		IRValue condition;
		IREnd* true_end = nullptr;
		IREnd* false_end = nullptr;
	};

	struct IRContext {
		IRContext(Emitter& emitter, IAllocator& allocator)
			: stack(allocator)
			, emitter(emitter)
		{}

		StackArray<IRValue, 16> stack;
		i32 emitted_index = -1;
		IRNode* tail = nullptr;
		IRNode* head = nullptr;
		Emitter& emitter;
		struct Arg {
			i32 num;
			i32 offset;
		};
		Span<Arg> args;
		u32 register_allocator = 0;
		u32 num_immutables = 0;
		
		IRValue& stackValue(i32 idx) {
			return stack[stack.size() + idx];
		}

		void push(IRNode* node) {
			if (tail) tail->next = node;
			node->prev = tail;
			if (!head) head = node;
			tail = node;
		}

		void popStack(u32 num) {
			for (u32 i = 0; i < num; ++i) stack.pop();
		}
	};
	
	IRNode* compileBytecode(CompileContext& ctx, IRNode* ir, OutputMemoryStream& compiled) {
		IRNode* node = ir;
		auto writeValue = [&](IRValue& val) {
			DataStream tmp;
			tmp.type = val.type;
			tmp.index = val.index;
			tmp.value = val.value;
			compiled.write(tmp);
		};
		while (node) {
			switch (node->type) {
				case IRNode::IF: {
					auto* n = (IRIf*)node;
					compiled.write(n->false_end ? InstructionType::CMP_ELSE : InstructionType::CMP);
					writeValue(n->condition);
					compiled.write(u16(0));
					if (n->false_end) compiled.write(u16(0));

					compileBytecode(ctx, n->next, compiled);
					node = n->true_end;
					if (n->false_end) {
						compileBytecode(ctx, node->next, compiled);
						node = n->false_end;
					}
					break;
				}
				case IRNode::END: {
					compiled.write(InstructionType::END);
					return node;
				}
				case IRNode::OP: {
					auto* n = (IROp*)node;
					compiled.write(n->instruction);
					if (n->dst.type != DataStream::NONE) writeValue(n->dst);
					if (n->instruction == InstructionType::RAND) { // TODO make this not special case
						for (IRValue& arg : n->args) {
							compiled.write(arg.value);
						}
					}
					else if (n->instruction == InstructionType::EMIT) { // TODO make this not special case
						compiled.write(u32(n->args[0].index));
						node = compileBytecode(ctx, n->next, compiled);
					}
					else {
						for (IRValue& arg : n->args) {
							writeValue(arg);
						}
					}
					break;
				}
			}
			node = node->next;
		}
		return nullptr;
	}

	void optimizeIR(IRContext& ctx) {
		reorderIR(ctx);
		removeRedundancy(ctx);
	}

	void printIR(StringView path, IRContext& ctx) {
		StaticString<4096> tmp;
		auto append = [&](const IRValue& val) {
			switch (val.type) {
				case DataStream::NONE:
				case DataStream::ERROR: tmp.append("##ERROR##"); break;
				case DataStream::LITERAL: tmp.append(val.value); break;
				case DataStream::CHANNEL: tmp.append("CH", val.index); break;
				case DataStream::GLOBAL: tmp.append("GLOB", val.index); break;
				case DataStream::OUT: tmp.append("OUT", val.index); break;
				case DataStream::REGISTER: tmp.append("R", val.index); break;
				case DataStream::SYSTEM_VALUE: tmp.append("SYS", val.index); break;
			}
			};
		IRNode* node = ctx.head;
		while (node) {
			switch (node->type) {
				case IRNode::OP: {
					auto* n = (IROp*)node;
					if (n->dst.type != DataStream::NONE) {
						append(n->dst);
						tmp.append(" = ");
					}

					auto appendArgs = [&](){
						for (IRValue& arg : n->args) {
							if (&arg != n->args.begin()) tmp.append(",");
							append(arg);
						}
						tmp.append(")");
					};
					
					switch (n->instruction) {
						case InstructionType::MUL: append(n->args[0]); tmp.append(" * "); append(n->args[1]); break;
						case InstructionType::ADD: append(n->args[0]); tmp.append(" + "); append(n->args[1]); break;
						case InstructionType::SUB: append(n->args[0]); tmp.append(" - "); append(n->args[1]); break;
						case InstructionType::DIV: append(n->args[0]); tmp.append(" / "); append(n->args[1]); break;
						case InstructionType::LT: append(n->args[0]); tmp.append(" < "); append(n->args[1]); break;
						case InstructionType::GT: append(n->args[0]); tmp.append(" > "); append(n->args[1]); break;
						case InstructionType::MOD: append(n->args[0]); tmp.append(" % "); append(n->args[1]); break;
						case InstructionType::COS: tmp.append("cos("); appendArgs(); break;
						case InstructionType::SIN: tmp.append("sin("); appendArgs(); break;
						case InstructionType::KILL: tmp.append("kill("); appendArgs(); break;
						case InstructionType::EMIT: tmp.append("emit(", n->args[0].index, ")"); break;
						case InstructionType::RAND: tmp.append("random("); appendArgs(); break;
						case InstructionType::MIN: tmp.append("min("); appendArgs(); break;
						case InstructionType::MAX: tmp.append("max("); appendArgs(); break;
						case InstructionType::NOISE: tmp.append("noise("); appendArgs(); break;
						case InstructionType::SQRT: tmp.append("sqrt("); appendArgs(); break;
						case InstructionType::MOV: append(n->args[0]); break;
						default: ASSERT(false); break;
					}
					break;
				}
				case IRNode::END:
					tmp.append("END");
					break;
				case IRNode::IF: {
					auto* n = (IRIf*)node;
					tmp.append("CMP ");
					append(n->condition);
					break;
				}
			}
			tmp.append("\n");
			node = node->next;
		}	
		logError(path, "\n\n");
		logError(tmp.data);
	}

	template <typename F>
	void forEachValue(IRNode* node, const F& fn) {
		u32 instruction_index = 0;
		while (node) {
			switch (node->type) {
				case IRNode::OP: {
					auto* n = (IROp*)node;
					if (n->dst.type != DataStream::NONE) fn(n->dst, true, instruction_index);
					for (IRValue& arg : n->args) {
						fn(arg, false, instruction_index);
					}
					break;
				}
				case IRNode::END: break;
				case IRNode::IF: {
					auto* n = (IRIf*)node;
					fn(n->condition, false, instruction_index);
					break;
				}
			}
			node = node->next;
			++instruction_index;
		}
	}

	// Optimizes register allocation by reusing registers whose lifetimes don't overlap.
	// Before this pass, each operation creates a unique register index. This pass remaps
	// those indices to minimize the total number of registers needed at runtime.
	u32 allocateRegisters(IRContext& ctx) {
		struct IRLifetime {
			u32 original_index;
			i32 from = -1;
			i32 to = -1;
			i32 remapped = -1;
		};

		StackArray<IRLifetime, 16> lifetimes(m_arena_allocator);
		HashMap<u32, i32> register_to_lifetime(m_allocator);

		// gather reads/writes
		forEachValue(ctx.head, [&](const IRValue& val, bool is_write, i32 instruction_index) {
			if (val.type == DataStream::REGISTER) {
				if (val.index < ctx.num_immutables) return;
				auto iter = register_to_lifetime.find(val.index);
				if (iter == register_to_lifetime.end()) {
					IRLifetime& lt = lifetimes.emplace();
					lt.original_index = val.index;
					lt.from = instruction_index;
					lt.to = instruction_index;
					register_to_lifetime.insert(val.index, lifetimes.size() - 1);
				} else {
					IRLifetime& lt = lifetimes[iter.value()];
					lt.to = maximum(lt.to, instruction_index);
				}
			}
		});

		u32 num_used_registers = 0;
		// assign remapped
		if (!lifetimes.empty()) {
			lifetimes[0].remapped = ctx.num_immutables;
			num_used_registers = 1;
		}
		for (i32 i = 1; i < lifetimes.size(); ++i) {
			IRLifetime& lt = lifetimes[i];
			lt.remapped = ctx.num_immutables;
			for (i32 j = 0; j < i; ++j) {
				const IRLifetime& prev = lifetimes[j];
				if (prev.remapped == lt.remapped && !(lt.to <= prev.from || prev.to <= lt.from)) {
					++lt.remapped;
					if (lt.remapped > 0xfe) {
						logError(m_path, ": Too many registers.");
						m_is_error = true;
						return 0;
					}
					j = -1; // restart check
					num_used_registers = maximum(num_used_registers, lt.remapped + 1);
				}
			}
		}

		// update indices
		forEachValue(ctx.head, [&](IRValue& val, bool, i32) {
			if (val.type != DataStream::REGISTER) return;
			if (val.index < ctx.num_immutables) return;
			auto iter = register_to_lifetime.find(val.index);
			val.index = lifetimes[iter.value()].remapped;
		});
		return num_used_registers;
	}

	static u32 getValues(IRNode& node, Span<IRValue> srcs, IRValue*& dst) {
		dst = nullptr;
		switch (node.type) {
			case IRNode::OP: {
				auto& n = (IROp&)node;
				dst = &n.dst;
				u32 i = 0;
				for (const IRValue& arg : n.args) {
					srcs[i] = arg;
					++i;
				}
				return i;
			}
			case IRNode::IF: {
				auto& n = (IRIf&)node;
				srcs[0] = n.condition;
				return 1;
			}
			case IRNode::END: return 0;
		}
		ASSERT(false);
		return 0;
	}

	enum class IRSwapResult {
		POSSIBLE,
		BLOCK,
		COLLISION
	};

	static IRSwapResult canSwap(IRNode& node, IRNode& node_dst) {
		switch (node_dst.type) {
			case IRNode::END: return IRSwapResult::BLOCK;
			case IRNode::IF: return IRSwapResult::BLOCK;
			case IRNode::OP: {
				auto& n = (IROp&)node_dst;
				if (n.instruction == InstructionType::EMIT) 
					return IRSwapResult::BLOCK;
				break;
			}
			default: break;
		}

		switch (node.type) {
			case IRNode::END: return IRSwapResult::BLOCK;
			case IRNode::IF: return IRSwapResult::BLOCK;
			case IRNode::OP: {
				auto& n = (IROp&)node;
				if (n.instruction == InstructionType::EMIT) 
					return IRSwapResult::BLOCK;
				break;
			}
			default: break;
		}

		IRValue values[9];
		IRValue* dst;
		IRValue prev_values[9];
		IRValue* prev_dst;
		u32 num_values = getValues(node, values, dst);
		u32 num_prev_values = getValues(node_dst, prev_values, prev_dst);

		if (dst) {
			for (u32 i = 0; i < num_prev_values; ++i) {
				if (*dst == prev_values[i]) return IRSwapResult::COLLISION;
			}
		}
		if (prev_dst) {
			for (u32 i = 0; i < num_values; ++i) {
				if (*prev_dst == values[i]) return IRSwapResult::COLLISION;
			}
		}
		return IRSwapResult::POSSIBLE;
	}

	// Reorders IR instructions to improve execution efficiency by moving instructions
	// earlier in the sequence when possible. This optimization reduces register pressure
	// and can enable better instruction scheduling by placing instructions closer to
	// where their results are first used.
	void reorderIR(IRContext& ctx) {
		if (!ctx.head) return;

		// we try to move nodes forward, otherwise conditions are moved far from ::CMP
		IRNode* node = ctx.tail;
		while (node) {
			IRNode* prev = node->prev;
			IRNode* dst = node->next;
			// to keep instructions roughly in the same order, we don't swap unless there's collision
			// this helps if we need to debug the compiler
			while (dst) {
				switch (canSwap(*node, *dst)) {
					case IRSwapResult::BLOCK: goto next;
					case IRSwapResult::POSSIBLE: break;
					case IRSwapResult::COLLISION: {
						if (dst->prev != node) {
							// Move node before dst (which is where it should stay)
							// Unlink node from its current position
							node->next->prev = node->prev;
							if (node->prev) node->prev->next = node->next;
							else ctx.head = node->next;

							// Insert node before dst
							node->next = dst;
							node->prev = dst->prev;
							
							dst->prev->next = node;
							dst->prev = node;
						}
						goto next;
					}
				}
				dst = dst->next;
			}
			next:
			node = prev;
		}
	}

	// Optimizes IR by removing redundant MOV instructions. This includes:
	// 1. Eliminating duplicate consecutive MOVs to the same destination
	// 2. removes a temporary register when an operation writes to it and then 
	//		immediately a MOV copies that result to the final destination.
	// 		If the temporary is written exactly once and read exactly once
	//		(by the MOV), the operation can write directly to the final 
	//		destination, eliminating the MOV entirely.
	void removeRedundancy(IRContext& ctx) {
		if (!ctx.head) return;

		IRNode* node = ctx.head;
		// keep only one mov from several movs in a row into the same destination
		while (node) {
			if (node->prev && node->type == IRNode::OP && node->prev->type == IRNode::OP) {
				auto* n_prev = (IROp*)node->prev;
				auto* n = (IROp*)node;
				if (n_prev->dst.type == n->dst.type && n_prev->dst.index == n->dst.index && n->instruction == InstructionType::MOV && n_prev->instruction == InstructionType::MOV) {
					if (n_prev == ctx.head) ctx.head = n_prev->next;
					if (n_prev->prev) n_prev->prev->next = n_prev->next;
					if (n_prev->next) n_prev->next->prev = n_prev->prev;
					if (ctx.head == n_prev) ctx.head = n;
				}
			}
			node = node->next;
		}
		
		struct RegisterAccess {
			u32 reads = 0;
			u32 writes = 0;
			IRNode* prev_writer = nullptr;
			IRValue alias;
			bool is_aliased = false;
		};
		StackArray<RegisterAccess, 16> register_access(m_arena_allocator);
		register_access.reserve(ctx.register_allocator);
		
		auto set = [&register_access](bool is_write, const IRValue& val) {
			if (val.type != DataStream::REGISTER) return;
			if (val.index >= (u32)register_access.size()) register_access.resize(val.index + 1);
			if (is_write) {
				++register_access[val.index].writes;
			}
			else {
				++register_access[val.index].reads;
			}
		};

		node = ctx.head;
		while (node) {
			IRValue srcs[9];
			IRValue* dst;
			u32 num_srcs = getValues(*node, srcs, dst); 
			if (dst) {
				set(true, *dst);
			}
			for (u32 i = 0; i < num_srcs; ++i) {
				set(false, srcs[i]);
			}
			node = node->next;
		}

		auto foldNode = [&](IRNode* node, const IRValue& dst, const IRValue& src) {
			if (dst.type == DataStream::REGISTER 
				&& register_access[dst.index].writes == 1 
				&& register_access[dst.index].reads == 1
			) {
				// alias
				register_access[dst.index].alias = src;
				register_access[dst.index].is_aliased = true;

				if (ctx.head == node) ctx.head = node->next;
				if (ctx.tail == node) ctx.tail = node->prev;
				if (node->prev) node->prev->next = node->next;
				if (node->next) node->next->prev = node->prev;
				return;
			}

			// replace with mov
			auto* mov = LUMIX_NEW(m_arena_allocator, IROp)(node->ast, m_arena_allocator);
			mov->instruction = InstructionType::MOV;
			mov->dst = dst;
			mov->args.push(src);
			mov->prev = node->prev;
			mov->next = node->next;
			if (ctx.head == node) ctx.head = mov;
			if (ctx.tail == node) ctx.tail = mov;
			if (node->prev) node->prev->next = mov;
			if (node->next) node->next->prev = mov;
		};

		// fold constants and collapse
		// op op_dst op_a op_b
		// mov mov_dst op_dst
		// into 
		// op mov_dst op_a op_b
		node = ctx.head;
		while (node) {
			switch (node->type) {
				case IRNode::OP: {
					auto* n = (IROp*)node;

					// fold all arguments first
					bool all_args_literals = true;
					for (IRValue& arg : n->args) {
						if (arg.type == DataStream::REGISTER && register_access[arg.index].is_aliased) {
							arg = register_access[arg.index].alias;
						}
						if (arg.type != DataStream::LITERAL) all_args_literals = false;
					}

					// fold operations like
					// a = b * 1
					// a = b * 0
					// a = b - 0
					if (n->args.size() == 2) {
						if (n->args[0].type == DataStream::LITERAL) {
							if ((n->instruction == InstructionType::ADD && n->args[0].value == 0)
								|| (n->instruction == InstructionType::MUL && n->args[0].value == 1)
								)
							{
								foldNode(n, n->dst, n->args[1]);
								break;
							}
							if (n->instruction == InstructionType::MUL && n->args[0].value == 0) {
								foldNode(n, n->dst, n->args[0]);
								break;
							}
						}

						if (n->args[1].type == DataStream::LITERAL) {
							if ((n->instruction == InstructionType::ADD && n->args[1].value == 0)
								|| (n->instruction == InstructionType::SUB && n->args[1].value == 0)
								|| (n->instruction == InstructionType::MUL && n->args[1].value == 1)
								|| (n->instruction == InstructionType::DIV && n->args[1].value == 1)
								)
							{
								foldNode(n, n->dst, n->args[0]);
								break;
							}
							if (n->instruction == InstructionType::MUL && n->args[1].value == 0) {
								foldNode(n, n->dst, n->args[1]);
								break;
							}
						}
					}

					// fold 
					// op regN a b
					// mov dst regN
					// into
					// op dst a b
					// if it's safe to do
					if (n->instruction == InstructionType::MOV
						&& n->args[0].type == DataStream::REGISTER 
						&& register_access[n->args[0].index].reads == 1
						&& register_access[n->args[0].index].writes == 1
						&& register_access[n->args[0].index].prev_writer
					) {
						IRNode* prev_writer = register_access[n->args[0].index].prev_writer;
						if (prev_writer->type == IRNode::OP) {
							auto* src = (IROp*)prev_writer;
							src->dst = n->dst;
							if (n->dst.type == DataStream::REGISTER) register_access[n->dst.index].prev_writer = prev_writer;
							if (n->prev) n->prev->next = n->next;
							if (n->next) n->next->prev = n->prev;
							if (ctx.tail == n) ctx.tail = n->prev;
							break;
						}
					}

					// fold the operation if all arguments are runtime constant
					// some operations can not be fold like this, e.g. random(...)
					if (n->dst.type != DataStream::NONE
						&& n->args.size() > 0
						&& all_args_literals
						&& n->dst.type == DataStream::REGISTER
						&& register_access[n->dst.index].writes == 1
					) {
						float first = n->args[0].value;
						switch (n->instruction) {
							case InstructionType::SQRT:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = sqrtf(n->args[0].value);
								break;
							case InstructionType::COS:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = cosf(n->args[0].value);
								break;
							case InstructionType::SIN:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = sinf(n->args[0].value);
								break;
							case InstructionType::ADD:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = first + n->args[1].value;
								break;
							case InstructionType::MUL:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = first * n->args[1].value;
								break;
							case InstructionType::DIV:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = first / n->args[1].value;
								break;
							case InstructionType::SUB:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = first - n->args[1].value;
								break;
							case InstructionType::MOD:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = fmodf(first, n->args[1].value);
								break;
							case InstructionType::LT:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = first < n->args[1].value ? 1.f : 0.f;
								break;
							case InstructionType::GT:
								register_access[n->dst.index].is_aliased = true;
								register_access[n->dst.index].alias.value = first > n->args[1].value ? 1.f : 0.f;
								break;
							default: break;
						}

						if (register_access[n->dst.index].is_aliased) {
							register_access[n->dst.index].alias.type = DataStream::LITERAL;
							if (n->prev) n->prev->next = n->next;
							if (n->next) n->next->prev = n->prev;
							if (ctx.tail == n) ctx.tail = n->prev;
							break;
						}
					}
					if (n->dst.type == DataStream::REGISTER) register_access[n->dst.index].prev_writer = n;
					break;
				}
				case IRNode::IF: break;
				case IRNode::END: break;
			}
			node = node->next;
		}
	}

	i32 compileIR(IRContext& ctx, Node* node) {
		switch (node->type) {
			case Node::EMITTER_REF: {
				auto* n = (EmitterRefNode*)node;
				ctx.stack.emplace().index = n->index;
				return 1;
			}
			case Node::COMPOUND: {
				auto* n = (CompoundNode*)node;
				i32 num = 0;
				for (u32 i = 0, c = n->elements.size(); i < c; ++i) {
					i32 r = compileIR(ctx, n->elements[i]);
					if (r < 0) return -1;
					ASSERT(r != 0); // TODO can this happen?
					num += r;
				}
				ASSERT(num <= 4); // TODO make sure this is handled in parser 
				return num;
			}
			case Node::SYSCALL: {
				auto* n = (SysCallNode*)node;
				auto* res = LUMIX_NEW(m_arena_allocator, IROp)(node, m_arena_allocator);
				res->instruction = n->function.instruction;
				res->args.reserve(n->args.size());
				for (Node* arg : n->args) {
					i32 a = compileIR(ctx, arg);
					switch (a) {
						case -1: return -1;
						case 1: break;
						default:
							error(node->token.value, "Arguments must be scalars.");
							return -1;
					}
					res->args.push(ctx.stack.last());
					ctx.stack.pop();
				}
				if (n->function.returns_value) {
					res->dst.type = DataStream::REGISTER;
					res->dst.index = ++ctx.register_allocator;
					ctx.stack.push(res->dst);
				}
				ctx.push(res);
				if (n->after_block) {
					ASSERT(n->function.instruction == InstructionType::EMIT);
					ctx.emitted_index = res->args[0].index;
					i32 a = compileIR(ctx, n->after_block);
					if (a < 0) return -1;
					// TODO a > 0

					ctx.emitted_index = -1;
					auto* end = LUMIX_NEW(m_arena_allocator, IREnd)(n->after_block);
					ctx.push(end);
				}
				else if (n->function.instruction == InstructionType::EMIT) {
					auto* end = LUMIX_NEW(m_arena_allocator, IREnd)(n);
					ctx.push(end);
				}
				return n->function.returns_value ? 1 : 0;
			}
			case Node::SWIZZLE: {
				auto* n = (SwizzleNode*)node;
				i32 left = compileIR(ctx, n->left);
				if (left < 0) return -1;

				StringView swizzle = n->token.value;
				IRValue swizzled[4];
				for (u32 i = 0; i < swizzle.size(); ++i) {
					i32 idx = -1;
					switch (swizzle[i]) {
						case 'x': case 'r': idx = 0; break;
						case 'y': case 'g': idx = 1; break;
						case 'z': case 'b': idx = 2; break;
						case 'w': case 'a': idx = 3; break;
						default: ASSERT(false); return -1;
					}
					if (idx >= left) {
						error(n->token.value, "Invalid swizzle component.");
						return -1;
					}
					swizzled[i] = ctx.stackValue(-left + idx);
				}
				ctx.popStack(left);
				for (u32 i = 0; i < swizzle.size(); ++i) ctx.stack.push(swizzled[i]);
				return swizzle.size();
			}
			case Node::SYSTEM_VALUE: {
				auto* n = (SystemValueNode*)node;
				IRValue& val = ctx.stack.emplace();
				val.type = DataStream::SYSTEM_VALUE;
				val.index = (u8)n->value;
				return 1;
			}
			case Node::FUNCTION_ARG: {
				auto* n = (FunctionArgNode*)node;
				IRContext::Arg arg = ctx.args[n->index];
				for (i32 i = 0; i < arg.num; ++i) {
					IRValue val = ctx.stack[ctx.args[n->index].offset + i];
					ctx.stack.push(val);
				}
				return arg.num;
			}
			case Node::FUNCTION_CALL: {
				auto* n = (FunctionCallNode*)node;
				const Function& fn = m_functions[n->function_index];
				IRContext::Arg args[8];
				if ((u32)n->args.size() > lengthOf(args)) {
					error(n->token.value, "Too many arguments.");
					return -1;
				}
				u32 arg_offset = ctx.stack.size();
				u32 args_size = 0;
				for (i32 i = 0; i < n->args.size(); ++i) {
					args[i].offset = arg_offset;
					args[i].num = compileIR(ctx, n->args[i]);
					args_size += args[i].num;
					if (args[i].num < 0) return -1;
					arg_offset = ctx.stack.size();
				}
				Span<IRContext::Arg> prev_args = ctx.args;
				ctx.args = { args, &args[n->args.size()] };
				i32 ret = compileIR(ctx, fn.block);
				ctx.args = prev_args;
				IRValue ret_vals[4];
				for (i32 i = 0; i < ret; ++i) {
					ret_vals[i] = ctx.stack.last();
					ctx.stack.pop();
				}
				ctx.popStack(args_size);
				for (i32 i = 0; i < ret; ++i) {
					ctx.stack.push(ret_vals[ret - i - 1]);
				}
				return ret;
			}
			case Node::VARIABLE: {
				auto* n = (VariableNode*)node;
				switch (n->family) {
					case VariableFamily::LOCAL: {
						Local& l = n->block->locals[n->index];
						u32 num = toCount(l.type);
						for (u32 i = 0; i < num; ++i) {
							IRValue& val = ctx.stack.emplace();
							val.type = DataStream::REGISTER;
							if (l.registers[i] < 0) {
								l.registers[i] = ++ctx.register_allocator;
							}
							val.index = l.registers[i];
						}
						return num;
					}
					case VariableFamily::INPUT: {
						u32 num;
						if (ctx.emitted_index >= 0) {
							Variable& v = m_emitters[ctx.emitted_index].m_inputs[n->index];
							num = toCount(v.type);
							for (u32 i = 0; i < num; ++i) {
								IRValue& val = ctx.stack.emplace();
								val.type = DataStream::OUT;
								val.index = v.getOffsetSub(i);
							}
						}
						else {
							Variable& v = ctx.emitter.m_inputs[n->index];
							num = toCount(v.type);
							for (u32 i = 0; i < num; ++i) {
								IRValue& val = ctx.stack.emplace();
								val.type = DataStream::REGISTER;
								val.index = v.getOffsetSub(i);
							}
						}
						return num;
					}
					case VariableFamily::CHANNEL: {
						Variable& v = ctx.emitter.m_vars[n->index];
						u32 num = toCount(v.type);
						for (u32 i = 0; i < num; ++i) {
							IRValue& val = ctx.stack.emplace();
							val.type = DataStream::CHANNEL;
							val.index = v.getOffsetSub(i);
						}
						return num;
					}
					case VariableFamily::GLOBAL: {
						const Variable& var = m_globals[n->index];
						u32 num = toCount(var.type);
						for (u32 i = 0; i < num; ++i) {
							IRValue& val = ctx.stack.emplace();
							val.type = DataStream::GLOBAL;
							val.index = var.getOffsetSub(i);
						}
						return num;
					}
					case VariableFamily::OUTPUT: {
						Variable& v = ctx.emitter.m_outputs[n->index];
						u32 num = toCount(v.type);
						for (u32 i = 0; i < num; ++i) {
							IRValue& val = ctx.stack.emplace();
							val.type = DataStream::OUT;
							val.index = v.getOffsetSub(i);
						}
						return num;
					}
					default: break;
				}
				ASSERT(false);
				return -1;
			}
			case Node::BINARY_OPERATOR: {
				auto* n = (BinaryOperatorNode*)node;
				i32 left = compileIR(ctx, n->left);
				i32 right = compileIR(ctx, n->right);
				if (left < 0 || right < 0) return -1;
				if (right != left && right != 1 && left != 1) {
					error(node->token.value, "Type mismatch.");
					return -1;
				}
				i32 num = maximum(left, right);
				IROp* ir_ops[4];
				for (i32 i = 0; i < num; ++i) {
					auto* res = LUMIX_NEW(m_arena_allocator, IROp)(n, m_arena_allocator);
					res->instruction = toInstruction(n->op);
					res->dst.type = DataStream::REGISTER;
					res->args.push(ctx.stackValue(-right - left + (left == 1 ? 0 : i)));
					res->args.push(ctx.stackValue(-right + (right == 1 ? 0 : i)));
					ir_ops[i] = res;
				}
				ctx.popStack(left + right);
				for (i32 i = 0; i < left; ++i) {
					ir_ops[i]->dst.index = ++ctx.register_allocator;
					ctx.stack.push(ir_ops[i]->dst);
					ctx.push(ir_ops[i]);
				}
				return num;
			}
			case Node::IF: {
				auto* n = (IfNode*)node;
				i32 cond = compileIR(ctx, n->condition);
				auto* res = LUMIX_NEW(m_arena_allocator, IRIf)(n);
				ctx.push(res);
				if (cond < 0) return -1;
				if (cond > 1) {
					error(n->token.value, "Condition must be scalar.");
					return -1;
				}
				res->condition = ctx.stack.last();
				ctx.stack.pop();
				
				i32 t = compileIR(ctx, n->true_block);
				if (t < 0) return -1; // TODO t > 0

				res->true_end = LUMIX_NEW(m_arena_allocator, IREnd)(n->true_block);
				ctx.push(res->true_end);

				if (n->false_block) {
					i32 f = compileIR(ctx, n->false_block);
					if (f < 0) return -1; // TODO f > 0

					res->false_end = LUMIX_NEW(m_arena_allocator, IREnd)(n->false_block);
					ctx.push(res->false_end);
				}
				return 0;
			}

			case Node::RETURN: {
				auto* n = (ReturnNode*)node;
				return compileIR(ctx, n->value);
			}
			case Node::LITERAL: {
				IRValue& val = ctx.stack.emplace();
				val.type = DataStream::LITERAL;
				val.value = ((LiteralNode*)node)->value;
				return 1;
			}
			case Node::BLOCK: {
				auto* n = (BlockNode*)node;
				u32 size_locals = 0;
				for (Node* statement : n->statements) {
					i32 s = compileIR(ctx, statement);
					if (s < 0) return -1;
					if (s > 0) {
						ctx.popStack(size_locals);
						return s; // TODO make sure this is from "return ...;"
					}
				}
				return 0;
			}
			case Node::ASSIGN: {
				auto* n = (AssignNode*)node;
				i32 left = compileIR(ctx, n->left);
				i32 right = compileIR(ctx, n->right);
				if (left < 0 || right < 0) return -1;
				if ((right < left && right != 1) || right > left) {
					error(node->token.value, "Type mismatch.");
					return -1;
				}

				IROp* movs[4];
				for (i32 i = 0; i < left; ++i) {
					auto* res = LUMIX_NEW(m_arena_allocator, IROp)(n, m_arena_allocator);
					res->instruction = InstructionType::MOV;
					res->dst = ctx.stackValue(-left - right + i);
					res->args.push(ctx.stackValue(-right + (right == 1 ? 0 : i)));
					movs[i] = res;
				}
				ctx.popStack(left + right);
				for (i32 i = 0; i < left; ++i) ctx.push(movs[i]);
				return 0;
			}
			default: break;
		}
		ASSERT(false);
		return -1;
	}

	void compileFunction(Emitter& emitter) {
		StringView fn_name;
		if (!consume(Token::IDENTIFIER, fn_name)) return;
		if (!consume(Token::LEFT_PAREN)) return;
		if (!consume(Token::RIGHT_PAREN)) return;
	
		CompileContext ctx(*this);
		ctx.emitter = &emitter;
		IRContext irctx(emitter, m_arena_allocator);

		u32 num_immutables = 0;
		OutputMemoryStream& compiled = [&]() -> OutputMemoryStream& {
			if (equalStrings(fn_name, "update")) {
				ctx.entry_point = EntryPoint::UPDATE;
				return emitter.m_update;
			}
			if (equalStrings(fn_name, "emit")) {
				ctx.entry_point = EntryPoint::EMIT;
				for (const Variable& v : emitter.m_inputs) {
					switch (v.type) {
						case ValueType::VOID: ASSERT(false); break;
						case ValueType::FLOAT: ++num_immutables; break;
						case ValueType::FLOAT3: num_immutables += 3; break;
						case ValueType::FLOAT4: num_immutables += 4; break;
					}
				}
				return emitter.m_emit;
			}
			if (equalStrings(fn_name, "output")) {
				ctx.entry_point = EntryPoint::OUTPUT;
				return emitter.m_output;
			}
			error(fn_name, "Unknown function");
			return emitter.m_output;
		}();
		if (m_is_error) return;

		irctx.register_allocator = num_immutables;
		irctx.num_immutables = num_immutables;

		BlockNode* b = block(ctx);
		if (!b || m_is_error) return;
		Node* collapsed = collapseConstants(b, nullptr, false);
		compileIR(irctx, collapsed);
		optimizeIR(irctx);
		u32 num_used_registers = allocateRegisters(irctx);
		//printIR(fn_name, irctx);
		compileBytecode(ctx, irctx.head, compiled);
		compiled.write(InstructionType::END);
		patchBlockSizes(compiled);
		InputMemoryStream ip(compiled.data(), compiled.size());
		u32 max_instruction_index = 0;
		forEachDataStreamInBytecode(ip, [&](const DataStream&, i32, i32, InstructionType, i32, i32 instruction_index){
			max_instruction_index = instruction_index;
		});

		if (equalStrings(fn_name, "update")) {
			emitter.m_num_update_registers = num_used_registers;
			emitter.m_num_update_instructions = max_instruction_index + 1;
		}
		else if (equalStrings(fn_name, "emit")) {
			emitter.m_num_emit_registers = num_used_registers;
			emitter.m_num_emit_instructions = max_instruction_index + 1;
		}
		else {
			emitter.m_num_output_registers = num_used_registers;
			emitter.m_num_output_instructions = max_instruction_index + 1;
		}
	}

	DataStream pushStack(CompileContext& ctx) {
		DataStream res;
		res.type = DataStream::REGISTER;
		res.index = ctx.value_counter;
		++ctx.value_counter;
		ASSERT(ctx.value_counter < 255);
		return res;
	}

	InstructionType toInstruction(Operators op) {
		switch (op) {
			case Operators::MOD: return InstructionType::MOD;
			case Operators::ADD: return InstructionType::ADD;
			case Operators::SUB: return InstructionType::SUB;
			case Operators::MUL: return InstructionType::MUL;
			case Operators::DIV: return InstructionType::DIV;
			case Operators::LT: return InstructionType::LT;
			case Operators::GT: return InstructionType::GT;
		}
		ASSERT(false);
		return InstructionType::END;
	}

	void parseFunction() {
		Function& fn = m_functions.emplace(m_allocator);
		if (!consume(Token::IDENTIFIER, fn.name)) return;
		parseArgs(fn);
		
		CompileContext ctx(*this);
		ctx.entry_point = EntryPoint::GLOBAL;
		ctx.function = &fn;
		fn.block = block(ctx);
		if (!fn.block) return;
		collapseConstants(fn.block, nullptr, false);
	}

	void compileMesh(Emitter& emitter) {
		StringView value;
		if (!consume(Token::STRING, value)) return;
		emitter.m_mesh = value;
	}

	void compileMaterial(Emitter& emitter) {
		StringView value;
		if (!consume(Token::STRING, value)) return;
		emitter.m_material = value;
	}

	float consumeFloat() {
		return asFloat(consumeToken());
	}

	u32 consumeU32() {
		Token t = consumeToken();
		if (t.type != Token::NUMBER) {
			error(t.value, "Expected number.");
			return 0;
		}
		u32 res;
		const char* end = fromCString(t.value, res);
		if (end != t.value.end) {
			error(t.value, "Expected u32.");
			return res;
		}
		return res;
	}

	void compileEmitter() {
		Emitter& emitter = m_emitters.emplace(m_allocator);
		if (!consume(Token::IDENTIFIER, emitter.m_name)) return;
		if (!consume(Token::LEFT_BRACE)) return;

		for (;;) {
			Token token = consumeToken();
			switch (token.type) {
				case Token::ERROR: return;
				case Token::FN: compileFunction(emitter); break;
				case Token::VAR: variableDeclaration(emitter.m_vars); break;
				case Token::OUT: variableDeclaration(emitter.m_outputs); break;
				case Token::IN: variableDeclaration(emitter.m_inputs); break;
				case Token::EOF:
					error(token.value, "Unexpected end of file.");
					return;
				case Token::RIGHT_BRACE:
					if (emitter.m_max_ribbons > 0 && emitter.m_max_ribbon_length == 0) {
						error(token.value, "max_ribbon_length must be > 0 if max_ribbons is > 0");
					}
					return;
				case Token::IDENTIFIER:
					if (equalStrings(token.value, "material")) compileMaterial(emitter);
					else if (equalStrings(token.value, "mesh")) compileMesh(emitter);
					else if (equalStrings(token.value, "emit_on_move")) emitter.m_emit_on_move = true;
					else if (equalStrings(token.value, "init_emit_count")) emitter.m_init_emit_count = consumeU32();
					else if (equalStrings(token.value, "emit_per_second")) emitter.m_emit_per_second = consumeFloat();
					else if (equalStrings(token.value, "max_ribbons")) emitter.m_max_ribbons = consumeU32();
					else if (equalStrings(token.value, "max_ribbon_length")) emitter.m_max_ribbon_length = consumeU32();
					else if (equalStrings(token.value, "init_ribbons_count")) emitter.m_init_ribbons_count = consumeU32();
					else {
						error(token.value, "Unexpected identifier ", token.value);
						return;
					}
					break;
				default:
					error(token.value, "Unexpected token ", token.value);
					return;
			}
		}
	}

	void fillVertexDecl(const Emitter& emitter, gpu::VertexDecl& decl) {
		u32 offset = 0;
		for (const Variable& o : emitter.m_outputs) {
			switch (o.type) {
				case ValueType::VOID: ASSERT(false); break; 
				case ValueType::FLOAT: 
					decl.addAttribute(offset, 1, gpu::AttributeType::FLOAT, gpu::Attribute::INSTANCED);
					offset += sizeof(float);
					break;
				case ValueType::FLOAT3: 
					decl.addAttribute(offset, 3, gpu::AttributeType::FLOAT, gpu::Attribute::INSTANCED);
					offset += sizeof(Vec3);
					break;
				case ValueType::FLOAT4: 
					decl.addAttribute(offset, 4, gpu::AttributeType::FLOAT, gpu::Attribute::INSTANCED);
					offset += sizeof(Vec4);
					break;
			}
		}
	}

	bool compile(const Path& path, StringView code, OutputMemoryStream& output) {
		m_path = path;
		m_tokenizer.m_current = code.begin;
		m_tokenizer.m_document = code;
		m_tokenizer.m_current_token = m_tokenizer.nextToken();

		// TODO error reporting
		for (;;) {
			Token token = consumeToken();
			switch (token.type) {
				case Token::EOF: goto write_label;
				case Token::ERROR: return false;
				case Token::IMPORT: parseImport(); break;
				case Token::CONST: parseConst(); break;
				case Token::FN: parseFunction(); break;
				case Token::GLOBAL: variableDeclaration(m_globals); break;
				case Token::EMITTER: compileEmitter(); break;
				default: error(token.value, "Unexpected token ", token.value); return false;
			}
		}

		write_label:
		ParticleSystemResource::Header header;
		output.write(header);

		output.write(m_emitters.size());
		auto getCount = [](const auto& x){
			u32 c = 0;
			for (const auto& i : x) {
				switch (i.type) {
					case ValueType::FLOAT4: c+= 4; break;
					case ValueType::FLOAT3: c+= 3; break;
					case ValueType::FLOAT: c+= 1; break;
					case ValueType::VOID: ASSERT(false); break;
				}
			}
			return c;
		};

		for (const Emitter& emitter : m_emitters) {
			gpu::VertexDecl decl(gpu::PrimitiveType::TRIANGLE_STRIP);
			fillVertexDecl(emitter, decl);
			output.write(decl);
			output.writeString(emitter.m_material);
			output.writeString(emitter.m_mesh);
			const u32 count = u32(emitter.m_update.size() + emitter.m_emit.size() + emitter.m_output.size());
			output.write(count);
			output.write(emitter.m_update.data(), emitter.m_update.size());
			output.write(emitter.m_emit.data(), emitter.m_emit.size());
			output.write(emitter.m_output.data(), emitter.m_output.size());
			output.write((u32)emitter.m_update.size());
			output.write(u32(emitter.m_update.size() + emitter.m_emit.size()));

			output.write(getCount(emitter.m_vars));
			output.write(emitter.m_num_update_registers);
			output.write(emitter.m_num_emit_registers);
			output.write(emitter.m_num_output_registers);
			output.write(emitter.m_num_update_instructions);
			output.write(emitter.m_num_emit_instructions);
			output.write(emitter.m_num_output_instructions);
			output.write(getCount(emitter.m_outputs));
			output.write(emitter.m_init_emit_count);
			output.write(emitter.m_emit_per_second);
			output.write(getCount(emitter.m_inputs));
			output.write(emitter.m_max_ribbons);
			output.write(emitter.m_max_ribbon_length);
			output.write(emitter.m_init_ribbons_count);
			output.write(emitter.m_emit_on_move);
		}
		output.write(m_globals.size());
		for (const Variable& p : m_globals) {
			output.writeString(p.name);
			switch (p.type) {
				case ValueType::VOID: ASSERT(false); break;
				case ValueType::FLOAT: output.write(u32(1)); break;
				case ValueType::FLOAT3: output.write(u32(3)); break;
				case ValueType::FLOAT4: output.write(u32(4)); break;
			}
		}
		
		return !m_is_error;
	}

	StudioApp& m_app;
	IAllocator& m_allocator;
	ArenaAllocator m_arena_allocator;
	Path m_path;
	bool m_is_error = false;
	ParticleScriptTokenizer m_tokenizer;
	Array<Constant> m_constants;
	Array<Function> m_functions;
	Array<Variable> m_globals;
	Array<Emitter> m_emitters;
};


struct ParticleScriptImportEditorWindow : AssetEditorWindow {
	ParticleScriptImportEditorWindow(const Path& path, StudioApp& app)
		: AssetEditorWindow(app)
		, m_app(app)
		, m_path(path)
	{
		m_editor = createParticleScriptEditor(m_app);
		m_editor->focus();
			
		OutputMemoryStream blob(app.getAllocator());
		if (app.getEngine().getFileSystem().getContentSync(path, blob)) {
			StringView v((const char*)blob.data(), (u32)blob.size());
			m_editor->setText(v);
		}
	}

	void save() {
		OutputMemoryStream blob(m_app.getAllocator());
		m_editor->serializeText(blob);
		m_app.getAssetBrowser().saveResource(m_path, blob);
		m_dirty = false;
	}

	void fileChangedExternally() override {
		OutputMemoryStream tmp(m_app.getAllocator());
		OutputMemoryStream tmp2(m_app.getAllocator());
		m_editor->serializeText(tmp);
		FileSystem& fs = m_app.getEngine().getFileSystem();
		if (!fs.getContentSync(m_path, tmp2)) return;

		if (tmp.size() == tmp2.size() && memcmp(tmp.data(), tmp2.data(), tmp.size()) == 0) {
			m_dirty = false;
		}
	}

	void windowGUI() override {
		CommonActions& actions = m_app.getCommonActions();

		if (ImGui::BeginMenuBar()) {
			if (actions.save.iconButton(m_dirty, &m_app)) save();
			if (actions.open_externally.iconButton(true, &m_app)) m_app.getAssetBrowser().openInExternalEditor(m_path);
			if (actions.view_in_browser.iconButton(true, &m_app)) m_app.getAssetBrowser().locate(m_path);
			ImGui::EndMenuBar();
		}

		if (m_editor->gui("codeeditor", ImVec2(0, 0), m_app.getMonospaceFont(), m_app.getDefaultFont())) m_dirty = true;
	}
	
	const Path& getPath() override { return m_path; }
	const char* getName() const override { return "particle script import editor"; }

	StudioApp& m_app;
	UniquePtr<CodeEditor> m_editor;
	Path m_path;
};

struct ParticleScriptImportPlugin : EditorAssetPlugin {
	ParticleScriptImportPlugin(StudioApp& app, IAllocator& allocator)
		: EditorAssetPlugin("Particle script import", "pai", TYPE, app, allocator)
		, m_app(app)
	{}

	bool compile(const Path& src) override { return true; }
	void openEditor(const Path& path) override {
		UniquePtr<ParticleScriptImportEditorWindow> win = UniquePtr<ParticleScriptImportEditorWindow>::create(m_app.getAllocator(), path, m_app);
		m_app.getAssetBrowser().addWindow(win.move());
	}
	void createResource(OutputMemoryStream& blob) override {}

	StudioApp& m_app;
	static inline ResourceType TYPE = ResourceType("particle_script_import");
};

struct ParticleScriptPlugin final : AssetBrowser::IPlugin, AssetCompiler::IPlugin {
	ParticleScriptPlugin(StudioApp& app, IAllocator& allocator)
		: m_app(app)
		, m_allocator(allocator)
	{
		AssetCompiler& compiler = app.getAssetCompiler();
		compiler.registerExtension("pat", ParticleSystemResource::TYPE);
		const char* particle_emitter_exts[] = { "pat" };
		compiler.addPlugin(*this, Span(particle_emitter_exts));
		m_app.getAssetBrowser().addPlugin(*this, Span(particle_emitter_exts));

		m_app.getSettings().registerOption("particle_script_preview", &m_show_preview, "Particle script", "Show preview");
	}

	bool compile(const Path& src) override {
		FileSystem& fs = m_app.getEngine().getFileSystem();
		OutputMemoryStream src_data(m_app.getAllocator());
		if (!fs.getContentSync(src, src_data)) return false;
		if (src_data.empty()) return false;

		StringView content = { (const char*)src_data.data(), (const char*)src_data.data() + src_data.size() };
		ParticleScriptCompiler compiler(m_app, m_allocator);
		OutputMemoryStream output(m_app.getAllocator());
		if (!compiler.compile(src, content, output)) return false;

		return m_app.getAssetCompiler().writeCompiledResource(src, Span(output.data(), (i32)output.size()));
	}

	const char* getIcon() const override { return ICON_FA_FIRE; }

	void addSubresources(AssetCompiler& compiler, const Path& path, AtomicI32&) override {
		compiler.addResource(ParticleSystemResource::TYPE, path);
		
		OutputMemoryStream content(m_allocator);
		if (!m_app.getEngine().getFileSystem().getContentSync(path, content)) {
			logError("Failed to read ", path);
			return;
		}

		if (content.empty()) {
			logError(path, " is empty.");
			return;
		}

		ParticleScriptTokenizer tokenizer;
		tokenizer.m_document.begin = (const char*)content.data();
		tokenizer.m_document.end = tokenizer.m_document.begin + content.size();
		tokenizer.m_current = tokenizer.m_document.begin;
		tokenizer.m_current_token = tokenizer.nextToken();
		
		for (;;) {
			ParticleScriptTokenizer::Token token = tokenizer.m_current_token;
			tokenizer.m_current_token = tokenizer.nextToken();
			
			switch (token.type) {
				case ParticleScriptTokenizer::Token::EOF: return;
				case ParticleScriptTokenizer::Token::ERROR: return;
				case ParticleScriptTokenizer::Token::IMPORT: {
					ParticleScriptTokenizer::Token t = tokenizer.m_current_token;
					tokenizer.m_current_token = tokenizer.nextToken();
					if (t.type == ParticleScriptTokenizer::Token::STRING) {
						m_app.getAssetCompiler().registerDependency(path, Path(t.value));
					}
					break;
				}
				default: break;
			}
		}	
	}

	bool canCreateResource() const override { return true; }
	bool canMultiEdit() override { return false; }
	void createResource(struct OutputMemoryStream& content) override {
		content << R"#(
emitter Emitter0 {
	material "/engine/materials/particle.mat"
	init_emit_count 0
	emit_per_second 10
	
	out i_position : float3
	out i_scale : float
	out i_color : float4
	out i_rot : float
	out i_frame : float
	out i_emission : float

	var pos : float3
	var t : float

	fn update() {
		t = t + time_delta;
		if t > 1 {
			kill();
		}
	}
	fn emit() {
		pos.x = random(-1, 1);
		pos.y = random(0, 2);
		pos.z = random(-1, 1);
		t = 0;
	}
	fn output() {
		i_position = pos;
		i_scale = 0.1;
		i_color = {1, 0, 0, 1};
		i_rot = 0;
		i_frame = 0;
		i_emission = 10;
	}
}
		)#";
	}
	const char* getDefaultExtension() const override { return "pat"; }

	const char* getLabel() const override { return "Particle script"; }
	ResourceType getResourceType() const override { return ParticleSystemResource::TYPE; }
	
	void openEditor(const struct Path& path) override;

	IAllocator& m_allocator;
	StudioApp& m_app;
	bool m_show_preview = true;
};


struct ParticleScriptEditorWindow : AssetEditorWindow {
	ParticleScriptEditorWindow(const Path& path, ParticleScriptPlugin& plugin)
		: AssetEditorWindow(plugin.m_app)
		, m_app(plugin.m_app)
		, m_path(path)
		, m_viewer(plugin.m_app)
		, m_plugin(plugin)
	{
		m_editor = createParticleScriptEditor(m_app);
		m_editor->focus();

		OutputMemoryStream blob(m_app.getAllocator());
		if (m_app.getEngine().getFileSystem().getContentSync(path, blob)) {
			StringView v((const char*)blob.data(), (u32)blob.size());
			m_editor->setText(v);
		}

		World* world = m_viewer.m_world;
		m_preview_entity = world->createEntity({ 0, 0, 0 }, Quat::IDENTITY);
		world->createComponent(types::particle_emitter, m_preview_entity);
		RenderModule* module = (RenderModule*)world->getModule(types::particle_emitter);
		module->setParticleEmitterPath(m_preview_entity, m_path);

		m_viewer.m_viewport.pos = { 0, 2, 5 };
		m_viewer.m_viewport.rot = { 0, 0, 1, 0 };
	}

	void save() {
		OutputMemoryStream blob(m_app.getAllocator());
		m_editor->serializeText(blob);
		m_app.getAssetBrowser().saveResource(m_path, blob);
		m_dirty = false;
	}

	void fileChangedExternally() override {
		OutputMemoryStream tmp(m_app.getAllocator());
		OutputMemoryStream tmp2(m_app.getAllocator());
		m_editor->serializeText(tmp);
		FileSystem& fs = m_app.getEngine().getFileSystem();
		if (!fs.getContentSync(m_path, tmp2)) return;

		if (tmp.size() == tmp2.size() && memcmp(tmp.data(), tmp2.data(), tmp.size()) == 0) {
			m_dirty = false;
		}
	}

	void windowGUI() override {
		CommonActions& actions = m_app.getCommonActions();

		if (ImGui::BeginMenuBar()) {
			if (actions.save.iconButton(m_dirty, &m_app)) save();
			if (actions.open_externally.iconButton(true, &m_app)) m_app.getAssetBrowser().openInExternalEditor(m_path);
			if (actions.view_in_browser.iconButton(true, &m_app)) m_app.getAssetBrowser().locate(m_path);
			if (ImGuiEx::IconButton(ICON_FA_ANGLE_DOUBLE_RIGHT, "Toggle preview")) m_plugin.m_show_preview = !m_plugin.m_show_preview;
			if (ImGuiEx::IconButton(ICON_FA_BUG, "Debug")) { ASSERT(false); /*TODO*/ };
			ImGui::EndMenuBar();
		}

		if (m_plugin.m_show_preview) {
			float w = ImGui::GetContentRegionAvail().x / 2;
			if (ImGui::BeginChild("code_pane", ImVec2(w, 0), ImGuiChildFlags_ResizeX)) {
				if (m_editor->gui("codeeditor", ImVec2(0, 0), m_app.getMonospaceFont(), m_app.getDefaultFont())) m_dirty = true;
			}
			ImGui::EndChild();
			ImGui::SameLine();
			if (ImGui::BeginChild("preview_pane")) {
				auto* module = (RenderModule*)m_viewer.m_world->getModule(types::particle_emitter);
				ParticleSystem& system = module->getParticleEmitter(m_preview_entity);

				if (ImGuiEx::IconButton(ICON_FA_INFO_CIRCLE, "Info")) m_show_info = !m_show_info;
				ImGui::SameLine();
				if (m_play) {
					if (ImGuiEx::IconButton(ICON_FA_PAUSE, "Pause")) m_play = false;
					float td = m_app.getEngine().getLastTimeDelta();
					module->updateParticleEmitter(m_preview_entity, td);
				}
				else {
					if (ImGuiEx::IconButton(ICON_FA_PLAY, "Play")) m_play = true;
				}
				ImGui::SameLine();
				if (ImGuiEx::IconButton(ICON_FA_STEP_FORWARD, "Next frame")) {
					if (m_play) logError("Particle simulation must be paused.");
					else {
						float td = m_app.getEngine().getLastTimeDelta();
						module->updateParticleEmitter(m_preview_entity, td);
					}
				}

				ImGui::SameLine();
				if (ImGuiEx::IconButton(ICON_FA_EYE, "Toggle ground")) {
					m_show_ground = !m_show_ground;
					module->enableModelInstance(m_viewer.m_ground, m_show_ground);
				};

				ImGui::SameLine();
				if (ImGui::Button(ICON_FA_UNDO_ALT " Reset")) system.reset();
				u32 num_particles = 0;
				for (ParticleSystem::Emitter& emitter : system.getEmitters()) {
					if (emitter.resource_emitter.max_ribbons > 0) {
						for (const auto& ribbon : emitter.ribbons) num_particles += ribbon.length;
					}
					else {
						num_particles += emitter.particles_count;
					}
				}

				if (!system.m_globals.empty()) {
					ImGui::SameLine();
					if (ImGui::Button("Globals")) ImGui::OpenPopup("Globals");
					if (ImGui::BeginPopup("Globals")) {
						u32 offset = 0;
						for (const auto& p : system.getResource()->getGlobals()) {
							ImGui::PushID(&p);
							ImGuiEx::Label(p.name.c_str());
							ImGui::SetNextItemWidth(150);
							float* f = system.m_globals.begin() + offset;
							switch (p.num_floats) {
							case 1: ImGui::InputFloat("##v", f); break;
							case 2: ImGui::InputFloat2("##v", f); break;
							case 3: ImGui::InputFloat3("##v", f); break;
							case 4: ImGui::InputFloat4("##v", f); break;
							}
							offset += p.num_floats;
							ImGui::PopID();
						}
						ImGui::EndPopup();
					}
				}
				ImGui::SameLine();
				ImGui::Text("Particles: %d", num_particles);
				
				ImVec2 viewer_pos = ImGui::GetCursorScreenPos();
				m_viewer.gui();
				
				if (m_show_info) {
					ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove |
							 ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing;					
					ImGui::SetNextWindowPos(ImVec2(viewer_pos.x + 10, viewer_pos.y + 10), ImGuiCond_Always);
					if (ImGui::Begin("Emitter Info##overlay", &m_show_info, flags)) {
						Span<ParticleSystemResource::Emitter> emitters = system.getResource()->getEmitters();
						for (u32 i = 0, c = emitters.size(); i < c; ++i) {
							const auto& emitter = emitters[i];
							ImGui::Text("Emitter %d", i + 1);
							ImGui::Indent();
							ImGui::LabelText("Emit registers", "%d", emitter.emit_registers_count);
							ImGui::LabelText("Emit instructions", "%d", emitter.emit_instructions_count);
							ImGui::LabelText("Update registers", "%d", emitter.update_registers_count);
							ImGui::LabelText("Update instructions", "%d", emitter.update_instructions_count);
							ImGui::LabelText("Output registers", "%d", emitter.output_registers_count);
							ImGui::LabelText("Output instructions", "%d", emitter.output_instructions_count);
							ImGui::Unindent();
						}
						ImGui::End();
					}
				}
			}
			ImGui::EndChild();
		}
		else {
			if (m_editor->gui("codeeditor", ImVec2(0, 0), m_app.getMonospaceFont(), m_app.getDefaultFont())) m_dirty = true;
		}
	}

	const Path& getPath() override { return m_path; }
	const char* getName() const override { return "particle script editor"; }

	ParticleScriptPlugin& m_plugin;
	StudioApp& m_app;
	UniquePtr<CodeEditor> m_editor;
	WorldViewer m_viewer;
	Path m_path;
	EntityRef m_preview_entity;
	bool m_play = true;
	bool m_show_ground = true;
	bool m_show_info = false;
};

void ParticleScriptPlugin::openEditor(const struct Path& path) {
	UniquePtr<ParticleScriptEditorWindow> win = UniquePtr<ParticleScriptEditorWindow>::create(m_allocator, path, *this);
	m_app.getAssetBrowser().addWindow(win.move());
}


}